review gui, add convert docx to pdf

This commit is contained in:
VALLONGOL 2025-06-18 08:09:24 +02:00
parent c32d857f02
commit c6ad73d449
4 changed files with 471 additions and 143 deletions

View File

@ -0,0 +1 @@
Luca Vallongo,Win11_Dev/admin,Win11_Dev,18.06.2025 08:03,file:///C:/Users/admin/AppData/Roaming/LibreOffice/4;

Binary file not shown.

View File

@ -8,10 +8,23 @@ import docx
import pypandoc import pypandoc
import pdfkit import pdfkit
import markdown import markdown
import subprocess
from docx.enum.text import WD_BREAK
from docx2pdf import convert as convert_word # Import per la conversione via Word
from ..utils.logger import get_logger from ..utils.logger import get_logger
log = get_logger(__name__) log = get_logger(__name__)
# --- Custom Exceptions ---
class TemplatePlaceholderError(ValueError):
"""Custom exception raised when a required placeholder is missing in the DOCX template."""
pass
class ConverterNotFoundError(Exception):
"""Custom exception raised when no suitable DOCX to PDF converter is found."""
pass
# --- PDFKit Configuration --- # --- PDFKit Configuration ---
try: try:
config = pdfkit.configuration() config = pdfkit.configuration()
@ -23,25 +36,38 @@ except OSError:
log.info(f"pdfkit configured using fallback path: {WKHTMLTOPDF_PATH}") log.info(f"pdfkit configured using fallback path: {WKHTMLTOPDF_PATH}")
else: else:
config = None config = None
log.warning("wkhtmltopdf not found. PDF conversion may fail.") log.warning(
"wkhtmltopdf not found in PATH or fallback location. PDF conversion will fail."
)
# --- Helper Functions --- # --- Helper Functions (nessuna modifica qui) ---
def _get_document_title(markdown_text): def _get_document_title(markdown_text: str) -> str:
match = re.search(r"^\s*#\s+(.+)", markdown_text, re.MULTILINE) """Extracts the first heading (any level) from the markdown text to use as the title."""
return match.group(1).strip() if match else "Untitled Document" match = re.search(r"^\s*#+\s+(.+)", markdown_text, re.MULTILINE)
if match:
return match.group(1).strip()
return "Untitled Document"
def _split_markdown_by_revision_history(markdown_text, separator_heading="## Revision Record"): def _split_markdown_by_revision_history(markdown_text: str, separator_heading="## Revision Record") -> tuple[str, str]:
"""
Splits markdown text into revision history and main content.
The revision history section ends at the next heading.
"""
pattern = re.compile(f"({re.escape(separator_heading)}.*?)(?=\n#+)", re.DOTALL | re.S) pattern = re.compile(f"({re.escape(separator_heading)}.*?)(?=\n#+)", re.DOTALL | re.S)
match = pattern.search(markdown_text) match = pattern.search(markdown_text)
if not match: if not match:
log.warning(f"'{separator_heading}' section not found. No revision history will be added.") log.warning(f"'{separator_heading}' section not found. No revision history will be added.")
return "", markdown_text return "", markdown_text
rev_history_md = match.group(0).strip() rev_history_md = match.group(0).strip()
main_content_md = markdown_text.replace(rev_history_md, "").strip() main_content_md = markdown_text.replace(rev_history_md, "", 1).strip()
return rev_history_md, main_content_md return rev_history_md, main_content_md
def _replace_text_in_paragraph(paragraph, placeholders): def _replace_text_in_paragraph(paragraph, placeholders: dict[str, str]):
"""Replaces placeholder text within a single paragraph, preserving formatting."""
full_text = "".join(run.text for run in paragraph.runs) full_text = "".join(run.text for run in paragraph.runs)
if not any(key in full_text for key in placeholders): if not any(key in full_text for key in placeholders):
return return
@ -69,7 +95,8 @@ def _replace_text_in_paragraph(paragraph, placeholders):
if font.color and font.color.rgb: if font.color and font.color.rgb:
new_run.font.color.rgb = font.color.rgb new_run.font.color.rgb = font.color.rgb
def _replace_text_in_element(element, placeholders): def _replace_text_in_element(element, placeholders: dict[str, str]):
"""Recursively replaces placeholders in paragraphs and tables within an element."""
for p in element.paragraphs: for p in element.paragraphs:
_replace_text_in_paragraph(p, placeholders) _replace_text_in_paragraph(p, placeholders)
for table in element.tables: for table in element.tables:
@ -77,67 +104,126 @@ def _replace_text_in_element(element, placeholders):
for cell in row.cells: for cell in row.cells:
_replace_text_in_element(cell, placeholders) _replace_text_in_element(cell, placeholders)
def _replace_text_placeholders(doc, placeholders): def _replace_metadata_placeholders(doc: docx.Document, placeholders: dict[str, str]):
log.info(f"Replacing text placeholders: {list(placeholders.keys())}") """Replaces all metadata placeholders throughout the document's body, header, and footer."""
log.info(f"Replacing metadata placeholders: {list(placeholders.keys())}")
_replace_text_in_element(doc, placeholders) _replace_text_in_element(doc, placeholders)
for section in doc.sections: for section in doc.sections:
_replace_text_in_element(section.header, placeholders) _replace_text_in_element(section.header, placeholders)
_replace_text_in_element(section.footer, placeholders) _replace_text_in_element(section.footer, placeholders)
def _find_placeholder_paragraph(doc, placeholder): def _find_placeholder_paragraph(doc: docx.Document, placeholder: str):
"""Finds the first paragraph containing a given placeholder text."""
for p in doc.paragraphs: for p in doc.paragraphs:
if placeholder in "".join(run.text for run in p.runs): if placeholder in "".join(run.text for run in p.runs):
return p return p
return None return None
def _insert_docx_at_paragraph(paragraph, source_docx_path): def _insert_docx_at_paragraph(paragraph, source_docx_path: str):
"""Inserts content from a source DOCX file at a specific paragraph's location."""
parent = paragraph._p.getparent() parent = paragraph._p.getparent()
index = parent.index(paragraph._p) index = parent.index(paragraph._p)
source_doc = docx.Document(source_docx_path) source_doc = docx.Document(source_docx_path)
for element in source_doc.element.body: for element in source_doc.element.body:
parent.insert(index, element) parent.insert(index, element)
index += 1 index += 1
parent.remove(paragraph._p) parent.remove(paragraph._p)
# --- Main Conversion Function --- def _remove_paragraph(paragraph):
def convert_markdown(input_file, output_format, add_toc=False, template_path=None, metadata=None): """Removes a paragraph from its parent element."""
if not os.path.exists(input_file): if paragraph is None:
raise FileNotFoundError(f"Input file not found: {input_file}") return
parent = paragraph._p.getparent()
parent.remove(paragraph._p)
log.info(f"Starting conversion of '{os.path.basename(input_file)}' to {output_format}.") def _add_revision_table(doc: docx.Document, rev_history_md: str):
"""Parses a markdown table from the revision history text and adds it to the document."""
placeholder_p = _find_placeholder_paragraph(doc, "%%REVISION_RECORD%%")
if not placeholder_p:
log.warning("Revision record placeholder not found in template. Skipping.")
return
with open(input_file, 'r', encoding='utf-8') as f: if not rev_history_md:
markdown_text = f.read() log.info("No revision history content found. Removing placeholder.")
_remove_paragraph(placeholder_p)
return
# --- CORREZIONE LOGICA PDF --- lines = [line.strip() for line in rev_history_md.strip().split('\n')]
if output_format == "PDF":
output_file = os.path.splitext(input_file)[0] + ".pdf" table_lines = [line for line in lines if line.startswith('|') and not line.startswith('|:--')]
if not table_lines:
log.warning("Could not parse a markdown table from the revision history section.")
_remove_paragraph(placeholder_p)
return
table_data = []
for line in table_lines:
cells = [cell.strip() for cell in line.split('|')][1:-1]
table_data.append(cells)
if not table_data or len(table_data) < 1:
log.warning("Revision history table is empty.")
_remove_paragraph(placeholder_p)
return
log.info(f"Adding revision history table with {len(table_data)} rows.")
num_cols = len(table_data[0])
table = doc.add_table(rows=1, cols=num_cols)
table.style = 'Table Grid'
hdr_cells = table.rows[0].cells
for i, header_text in enumerate(table_data[0]):
hdr_cells[i].text = header_text
for row_data in table_data[1:]:
row_cells = table.add_row().cells
for i, cell_text in enumerate(row_data):
row_cells[i].text = cell_text
parent = placeholder_p._p.getparent()
parent.insert(parent.index(placeholder_p._p), table._tbl)
_remove_paragraph(placeholder_p)
# --- Format-Specific Conversion Functions ---
def _convert_to_pdf(markdown_text: str, output_file: str, add_toc: bool):
"""Converts markdown text to a PDF file."""
log.info("Starting PDF conversion using pdfkit.") log.info("Starting PDF conversion using pdfkit.")
if config is None: if config is None:
raise FileNotFoundError("wkhtmltopdf not found.") raise FileNotFoundError(
"wkhtmltopdf executable not found. Cannot create PDF."
)
md_converter = markdown.Markdown(extensions=['toc', 'fenced_code', 'tables'])
# Estrai il titolo dal testo markdown
title = _get_document_title(markdown_text) title = _get_document_title(markdown_text)
# Converti il corpo del testo content_without_title = markdown_text
html_body = md_converter.convert(markdown_text) match = re.search(r"^\s*#+\s+(.+)\n?", markdown_text, re.MULTILINE)
if match:
content_without_title = markdown_text[match.end():]
md_converter = markdown.Markdown(extensions=['toc', 'fenced_code', 'tables'])
html_body = md_converter.convert(content_without_title)
toc_html = "" toc_html = ""
# Genera il TOC se richiesto
if add_toc and hasattr(md_converter, 'toc') and md_converter.toc: if add_toc and hasattr(md_converter, 'toc') and md_converter.toc:
log.info("Generating Table of Contents for PDF.") log.info("Generating Table of Contents for PDF.")
# Mettiamo il TOC dopo il titolo principale, con un page-break toc_html = f"<h2>Table of Contents</h2>{md_converter.toc}<div style='page-break-after: always;'></div>"
toc_html = f"<div style='page-break-after: always;'><h2>Table of Contents</h2>{md_converter.toc}</div>"
# Costruisci l'HTML finale, usando il titolo estratto sia nel <title> che come <h1>
full_html = f""" full_html = f"""
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{title}</title> <title>{title}</title>
<style>
body {{ font-family: sans-serif; }}
h1, h2 {{ border-bottom: 1px solid #eaecef; padding-bottom: .3em; }}
</style>
</head> </head>
<body> <body>
<h1>{title}</h1> <h1>{title}</h1>
@ -147,51 +233,180 @@ def convert_markdown(input_file, output_format, add_toc=False, template_path=Non
</html> </html>
""" """
pdfkit.from_string(full_html, output_file, configuration=config, options={'encoding': "UTF-8"}) pdf_options = {'encoding': "UTF-8", 'enable-local-file-access': None}
pdfkit.from_string(full_html, output_file, configuration=config, options=pdf_options)
log.info(f"PDF successfully generated: {output_file}") log.info(f"PDF successfully generated: {output_file}")
elif output_format == "DOCX":
output_file = os.path.splitext(input_file)[0] + ".docx" def _convert_to_docx(markdown_text: str, output_file: str, template_path: str, metadata: dict, add_toc: bool):
if not template_path: """Converts markdown text to a DOCX file using a template."""
raise FileNotFoundError("A DOCX template file is required.") log.info("Starting DOCX conversion.")
if not template_path or not os.path.exists(template_path):
raise FileNotFoundError("A valid DOCX template file is required for this conversion.")
doc = docx.Document(template_path) doc = docx.Document(template_path)
if metadata: # --- Step 1: Validate Template Placeholders ---
rev_placeholder_p = _find_placeholder_paragraph(doc, "%%REVISION_RECORD%%")
toc_placeholder_p = _find_placeholder_paragraph(doc, "%%DOC_TOC%%")
content_placeholder_p = _find_placeholder_paragraph(doc, "%%DOC_CONTENT%%")
missing_placeholders = []
if not rev_placeholder_p:
missing_placeholders.append("%%REVISION_RECORD%%")
if not toc_placeholder_p:
missing_placeholders.append("%%DOC_TOC%%")
if not content_placeholder_p:
missing_placeholders.append("%%DOC_CONTENT%%")
if missing_placeholders:
raise TemplatePlaceholderError(
f"Template is missing required placeholders: {', '.join(missing_placeholders)}"
)
# --- Step 2: Replace Metadata ---
metadata['DOC_PROJECT'] = metadata.get('DOC_PROJECT') or _get_document_title(markdown_text) metadata['DOC_PROJECT'] = metadata.get('DOC_PROJECT') or _get_document_title(markdown_text)
placeholders = {f"%%{key}%%": value for key, value in metadata.items() if value} placeholders = {f"%%{key}%%": value for key, value in metadata.items() if value}
placeholders["%%DOC_DATE%%"] = date.today().strftime("%d/%m/%Y") placeholders["%%DOC_DATE%%"] = date.today().strftime("%d/%m/%Y")
_replace_text_placeholders(doc, placeholders) _replace_metadata_placeholders(doc, placeholders)
# --- Step 3: Split Markdown and Prepare for Insertion ---
rev_history_md, main_content_md = _split_markdown_by_revision_history(markdown_text) rev_history_md, main_content_md = _split_markdown_by_revision_history(markdown_text)
# --- Step 4: Add Revision History Table Natively ---
_add_revision_table(doc, rev_history_md)
temp_files = [] temp_files = []
try: try:
rev_placeholder_p = _find_placeholder_paragraph(doc, "%%REVISION_RECORD%%") # --- Step 5: Insert Main Content and TOC with Page Breaks ---
if rev_history_md and rev_placeholder_p: if main_content_md:
with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as temp_file: content_for_pandoc = main_content_md
pypandoc.convert_text(rev_history_md, 'docx', format='md', outputfile=temp_file.name) match = re.search(r"^\s*#\s+(.+)\n?", content_for_pandoc, re.MULTILINE)
temp_files.append(temp_file.name) if match:
_insert_docx_at_paragraph(rev_placeholder_p, temp_file.name) log.info("Removing main title from content to exclude it from DOCX TOC.")
content_for_pandoc = content_for_pandoc[match.end():]
content_placeholder_p = _find_placeholder_paragraph(doc, "%%DOC_CONTENT%%") log.info("Stripping manual numbering from headings.")
if main_content_md and content_placeholder_p: content_for_pandoc = re.sub(
pandoc_args = ["--toc"] if add_toc else [] r"^(\s*#+)\s+[0-9\.]+\s+",
with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as temp_file: r"\1 ",
pypandoc.convert_text(main_content_md, 'docx', format='md', extra_args=pandoc_args, outputfile=temp_file.name) content_for_pandoc,
temp_files.append(temp_file.name) flags=re.MULTILINE
)
content_placeholder_p.insert_paragraph_before().add_run().add_break(docx.enum.text.WD_BREAK.PAGE) pandoc_args = ["--shift-heading-level-by=-1"]
if add_toc:
pandoc_args.append("--toc")
log.info("Adding page break before Table of Contents.")
toc_placeholder_p.insert_paragraph_before().add_run().add_break(WD_BREAK.PAGE)
with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as temp_file:
pypandoc.convert_text(content_for_pandoc, 'docx', format='md', extra_args=pandoc_args, outputfile=temp_file.name)
temp_files.append(temp_file.name)
_insert_docx_at_paragraph(toc_placeholder_p, temp_file.name)
_remove_paragraph(content_placeholder_p)
else:
log.info("Adding page break before main content.")
content_placeholder_p.insert_paragraph_before().add_run().add_break(WD_BREAK.PAGE)
with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as temp_file:
pypandoc.convert_text(content_for_pandoc, 'docx', format='md', extra_args=pandoc_args, outputfile=temp_file.name)
temp_files.append(temp_file.name)
_insert_docx_at_paragraph(content_placeholder_p, temp_file.name) _insert_docx_at_paragraph(content_placeholder_p, temp_file.name)
_remove_paragraph(toc_placeholder_p)
else:
_remove_paragraph(toc_placeholder_p)
_remove_paragraph(content_placeholder_p)
doc.save(output_file) doc.save(output_file)
log.info(f"Document successfully created at {output_file}") log.info(f"Document successfully created at {output_file}")
finally: finally:
# --- Final Step: Cleanup ---
for temp_file in temp_files: for temp_file in temp_files:
if os.path.exists(temp_file): if os.path.exists(temp_file):
os.remove(temp_file) os.remove(temp_file)
def convert_docx_to_pdf(input_docx_path: str, output_pdf_path: str) -> str:
"""
Converts a DOCX file to a PDF, trying MS Word first and falling back to LibreOffice.
"""
if not os.path.exists(input_docx_path):
raise FileNotFoundError(f"Input DOCX file not found: {input_docx_path}")
# --- Strategy 1: Try to use Microsoft Word via COM ---
try:
log.info("Attempting DOCX to PDF conversion using MS Word.")
convert_word(input_docx_path, output_pdf_path)
log.info(f"Successfully converted using MS Word: {output_pdf_path}")
return output_pdf_path
except Exception as e:
log.warning(f"MS Word conversion failed. It might not be installed. Error: {e}")
log.info("Falling back to LibreOffice conversion.")
# --- Strategy 2: Fallback to LibreOffice ---
libreoffice_path = r"C:\Program Files\LibreOffice\program\soffice.exe"
if not os.path.exists(libreoffice_path):
log.error("LibreOffice executable not found. Cannot convert DOCX to PDF.")
raise ConverterNotFoundError(
"Neither MS Word nor LibreOffice could be used for conversion. "
"Please install one of them to use this feature."
)
# LibreOffice usa --outdir per la cartella, quindi dobbiamo estrarla.
output_dir = os.path.dirname(output_pdf_path)
log.info(f"Attempting conversion using LibreOffice at: {libreoffice_path}")
try:
# Per LibreOffice, dobbiamo convertire il file e poi rinominarlo se necessario,
# poiché non supporta la specifica di un nome file di output diretto.
expected_lo_output = os.path.join(output_dir, os.path.splitext(os.path.basename(input_docx_path))[0] + ".pdf")
command = [
libreoffice_path, "--headless", "--convert-to", "pdf",
"--outdir", output_dir, input_docx_path,
]
subprocess.run(
command, check=True, capture_output=True, text=True,
encoding='utf-8', errors='ignore'
)
# Se il nome file di output desiderato è diverso da quello generato da LibreOffice, rinominiamo.
if expected_lo_output != output_pdf_path and os.path.exists(expected_lo_output):
os.rename(expected_lo_output, output_pdf_path)
log.info(f"Successfully converted using LibreOffice: {output_pdf_path}")
return output_pdf_path
except (subprocess.CalledProcessError, FileNotFoundError) as e:
log.error(f"LibreOffice conversion failed. Error: {e}", exc_info=True)
raise e
# --- Main Conversion Dispatcher Function ---
def convert_markdown(input_file: str, output_path: str, output_format: str, add_toc: bool = False, template_path: str = None, metadata: dict = None):
"""
Converts a Markdown file to the specified output format (PDF or DOCX).
Writes directly to the specified output_path.
"""
if not os.path.exists(input_file):
raise FileNotFoundError(f"Input file not found: {input_file}")
log.info(f"Starting conversion of '{os.path.basename(input_file)}' to {output_format}.")
with open(input_file, 'r', encoding='utf-8') as f:
markdown_text = f.read()
if output_format == "PDF":
_convert_to_pdf(markdown_text, output_path, add_toc)
elif output_format == "DOCX":
if metadata is None:
metadata = {}
_convert_to_docx(markdown_text, output_path, template_path, metadata, add_toc)
else: else:
raise ValueError(f"Unsupported output format: {output_format}") raise ValueError(f"Unsupported output format: {output_format}")
return output_file return output_path

View File

@ -5,12 +5,13 @@ import sys
import logging import logging
import subprocess import subprocess
import tkinter as tk import tkinter as tk
from datetime import date
import ttkbootstrap as tb import ttkbootstrap as tb
from tkinter.scrolledtext import ScrolledText from tkinter.scrolledtext import ScrolledText
from ttkbootstrap.constants import * from ttkbootstrap.constants import *
from tkinter import filedialog, messagebox, StringVar, BooleanVar from tkinter import filedialog, messagebox, StringVar, BooleanVar
from ..core.core import convert_markdown from ..core.core import convert_markdown, convert_docx_to_pdf, ConverterNotFoundError
from ..utils.config import save_configuration, load_configuration from ..utils.config import save_configuration, load_configuration
from ..utils.logger import ( from ..utils.logger import (
setup_basic_logging, setup_basic_logging,
@ -23,8 +24,8 @@ from .editor import EditorWindow
log = get_logger(__name__) log = get_logger(__name__)
def open_with_default_app(filepath): def open_with_default_app(filepath):
if not filepath: if not filepath or not os.path.exists(filepath):
log.warning("Open file/folder requested, but no path was provided.") log.warning("Open file/folder requested, but path is invalid or not provided.")
messagebox.showwarning("Warning", "No output file or folder to open.") messagebox.showwarning("Warning", "No output file or folder to open.")
return return
try: try:
@ -51,7 +52,7 @@ def open_output_folder(filepath):
def run_app(): def run_app():
app = tb.Window(themename="sandstone") app = tb.Window(themename="sandstone")
app.title("Markdown Converter") app.title("Markdown Converter")
app.geometry("800x750") app.geometry("900x850")
app.resizable(True, True) app.resizable(True, True)
log_config = { log_config = {
@ -70,17 +71,56 @@ def run_app():
config = load_configuration() config = load_configuration()
metadata_config = config.get("metadata", {}) metadata_config = config.get("metadata", {})
# --- Variabili di stato della GUI ---
selected_file = StringVar(value=config.get("last_markdown_file", "")) selected_file = StringVar(value=config.get("last_markdown_file", ""))
selected_template = StringVar(value=config.get("last_template_file", "")) selected_template = StringVar(value=config.get("last_template_file", ""))
add_toc_var = BooleanVar(value=config.get("add_toc", True)) add_toc_var = BooleanVar(value=config.get("add_toc", True))
output_path = StringVar()
doc_security_var = StringVar(value=metadata_config.get("DOC_SECURITY", "")) doc_security_var = StringVar(value=metadata_config.get("DOC_SECURITY", ""))
doc_number_var = StringVar(value=metadata_config.get("DOC_NUMBER", "")) doc_number_var = StringVar(value=metadata_config.get("DOC_NUMBER", ""))
doc_rev_var = StringVar(value=metadata_config.get("DOC_REV", "")) doc_rev_var = StringVar(value=metadata_config.get("DOC_REV", ""))
doc_project_var = StringVar(value=metadata_config.get("DOC_PROJECT", "")) doc_project_var = StringVar(value=metadata_config.get("DOC_PROJECT", ""))
customer_var = StringVar(value=metadata_config.get("CUSTOMER", "")) customer_var = StringVar(value=metadata_config.get("DOC_CUSTOMER", ""))
docx_output_path = StringVar()
pdf_direct_output_path = StringVar()
pdf_from_docx_output_path = StringVar()
# --- Funzioni di supporto ---
def _confirm_overwrite(filepath: str) -> bool:
"""Checks if a file exists and asks the user for overwrite confirmation."""
if os.path.exists(filepath):
return messagebox.askyesno(
title="Confirm Overwrite",
message=f"The file already exists:\n\n{filepath}\n\nDo you want to overwrite it?"
)
return True # File doesn't exist, proceed.
def _update_output_paths(*args):
source_path = selected_file.get()
if not source_path:
return
output_dir = os.path.dirname(source_path)
project = doc_project_var.get() or "NoProject"
doc_num = doc_number_var.get() or "NoNum"
rev = doc_rev_var.get() or "NoRev"
today = date.today().strftime("%Y%m%d")
docx_filename = f"{project}_SUM_{doc_num}_{rev}_{today}.docx"
docx_output_path.set(os.path.join(output_dir, docx_filename).replace("\\", "/"))
pdf_from_docx_filename = f"{project}_SUM_{doc_num}_{rev}_{today}.pdf"
pdf_from_docx_output_path.set(os.path.join(output_dir, pdf_from_docx_filename).replace("\\", "/"))
pdf_direct_filename = f"{project}_SUM_{today}.pdf"
pdf_direct_output_path.set(os.path.join(output_dir, pdf_direct_filename).replace("\\", "/"))
for var in [selected_file, doc_project_var, doc_number_var, doc_rev_var]:
var.trace_add("write", _update_output_paths)
# --- Funzioni associate agli eventi della GUI ---
def open_editor(): def open_editor():
file_path = selected_file.get() file_path = selected_file.get()
if not file_path or not os.path.exists(file_path): if not file_path or not os.path.exists(file_path):
@ -94,17 +134,48 @@ def run_app():
selected_file.set(path) selected_file.set(path)
def browse_template(): def browse_template():
# --- CORREZIONE: Cerca prima i file .docx ---
path = filedialog.askopenfilename( path = filedialog.askopenfilename(
title="Select a Template Document", title="Select a Template Document",
filetypes=[("Word Documents", "*.docx"), ("All files", "*.*")] filetypes=[("Word Documents", "*.docx"), ("All files", "*.*")]
) )
if path: if path:
selected_template.set(path) selected_template.set(path)
edit_template_btn.config(state=tk.NORMAL)
docx_to_pdf_btn = None
def convert_from_docx_to_pdf():
input_path = docx_output_path.get()
output_path = pdf_from_docx_output_path.get()
if not input_path or not os.path.exists(input_path):
messagebox.showerror("Error", "The source DOCX file does not exist. Generate it first.")
return
if not output_path:
messagebox.showerror("Error", "Please specify a valid output path for the PDF.")
return
if not _confirm_overwrite(output_path):
return # User cancelled overwrite
try:
pdf_output = convert_docx_to_pdf(input_path, output_path)
messagebox.showinfo("Success", f"DOCX successfully converted to PDF:\n{pdf_output}")
except ConverterNotFoundError as e:
messagebox.showerror("Converter Not Found", str(e))
except Exception as e:
messagebox.showerror(
"DOCX to PDF Conversion Error",
f"An unexpected error occurred.\nError: {str(e)}"
)
def convert(fmt): def convert(fmt):
file_path = selected_file.get() nonlocal docx_to_pdf_btn
if not file_path: if docx_to_pdf_btn:
docx_to_pdf_btn.config(state=tk.DISABLED)
input_path = selected_file.get()
if not input_path:
messagebox.showerror("Error", "Please select a Markdown file.") messagebox.showerror("Error", "Please select a Markdown file.")
return return
@ -113,6 +184,19 @@ def run_app():
messagebox.showwarning("Warning", "A DOCX template is required.") messagebox.showwarning("Warning", "A DOCX template is required.")
return return
output_path_for_conversion = ""
if fmt == "DOCX":
output_path_for_conversion = docx_output_path.get()
elif fmt == "PDF":
output_path_for_conversion = pdf_direct_output_path.get()
if not output_path_for_conversion:
messagebox.showerror("Error", "Please specify a valid output path.")
return
if not _confirm_overwrite(output_path_for_conversion):
return # User cancelled overwrite
metadata_to_pass = { metadata_to_pass = {
'DOC_SECURITY': doc_security_var.get(), 'DOC_SECURITY': doc_security_var.get(),
'DOC_NUMBER': doc_number_var.get(), 'DOC_NUMBER': doc_number_var.get(),
@ -123,71 +207,99 @@ def run_app():
try: try:
output = convert_markdown( output = convert_markdown(
input_file=file_path, output_format=fmt, add_toc=add_toc_var.get(), input_file=input_path,
template_path=template, metadata=metadata_to_pass output_path=output_path_for_conversion, # Pass the final path to the core
output_format=fmt,
add_toc=add_toc_var.get(),
template_path=template,
metadata=metadata_to_pass,
) )
output_path.set(output)
messagebox.showinfo("Success", f"File converted successfully:\n{output}") messagebox.showinfo("Success", f"File converted successfully:\n{output}")
save_configuration( save_configuration(
last_markdown=file_path, last_template=template, last_markdown=input_path, last_template=template,
add_toc=add_toc_var.get(), metadata=metadata_to_pass add_toc=add_toc_var.get(), metadata=metadata_to_pass
) )
if fmt == "DOCX" and docx_to_pdf_btn:
docx_to_pdf_btn.config(state=tk.NORMAL)
except Exception as e: except Exception as e:
log.critical(f"An error occurred during conversion: {e}", exc_info=True) log.critical(f"An error occurred during conversion: {e}", exc_info=True)
messagebox.showerror("Error", f"An error occurred during conversion:\n{str(e)}") messagebox.showerror("Error", f"An error occurred during conversion:\n{str(e)}")
# --- Costruzione della GUI ---
container = tb.Frame(app, padding=10) container = tb.Frame(app, padding=10)
container.pack(fill=tk.BOTH, expand=True) container.pack(fill=tk.BOTH, expand=True)
top_frame = tb.Frame(container)
top_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10))
tb.Label(top_frame, text="Markdown File:").grid(row=0, column=0, padx=(0, 10), pady=5, sticky="w") input_frame = tb.Labelframe(container, text="Input Files", padding=10)
tb.Entry(top_frame, textvariable=selected_file, width=70).grid(row=0, column=1, padx=5, sticky="ew") input_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10))
file_button_frame = tb.Frame(top_frame) input_frame.columnconfigure(1, weight=1)
tb.Label(input_frame, text="Markdown File:").grid(row=0, column=0, padx=(0, 10), pady=5, sticky="w")
tb.Entry(input_frame, textvariable=selected_file).grid(row=0, column=1, padx=5, pady=5, sticky="ew")
file_button_frame = tb.Frame(input_frame)
file_button_frame.grid(row=0, column=2, sticky="w") file_button_frame.grid(row=0, column=2, sticky="w")
tb.Button(file_button_frame, text="Browse...", command=browse_markdown, bootstyle=PRIMARY).pack(side=tk.LEFT, padx=(0, 5)) tb.Button(file_button_frame, text="Browse...", command=browse_markdown, bootstyle=PRIMARY).pack(side=tk.LEFT, padx=(0, 5))
tb.Button(file_button_frame, text="Edit...", command=open_editor, bootstyle=INFO).pack(side=tk.LEFT) tb.Button(file_button_frame, text="Edit...", command=open_editor, bootstyle=INFO).pack(side=tk.LEFT)
tb.Label(top_frame, text="Template File:").grid(row=1, column=0, padx=(0, 10), pady=5, sticky="w") tb.Label(input_frame, text="Template File:").grid(row=1, column=0, padx=(0, 10), pady=5, sticky="w")
tb.Entry(top_frame, textvariable=selected_template, width=70).grid(row=1, column=1, padx=5, sticky="ew") tb.Entry(input_frame, textvariable=selected_template).grid(row=1, column=1, padx=5, pady=5, sticky="ew")
tb.Button(top_frame, text="Browse...", command=browse_template, bootstyle=SECONDARY).grid(row=1, column=2, padx=5) template_button_frame = tb.Frame(input_frame)
template_button_frame.grid(row=1, column=2, sticky="w")
tb.Button(template_button_frame, text="Browse...", command=browse_template, bootstyle=SECONDARY).pack(side=tk.LEFT, padx=(0, 5))
edit_template_btn = tb.Button(template_button_frame, text="Edit...", command=lambda: open_with_default_app(selected_template.get()), bootstyle="info", state=tk.DISABLED)
edit_template_btn.pack(side=tk.LEFT)
metadata_frame = tb.Labelframe(top_frame, text="Template Placeholders", padding=10) metadata_frame = tb.Labelframe(container, text="Template Placeholders", padding=10)
metadata_frame.grid(row=2, column=0, columnspan=3, padx=0, pady=10, sticky="ew") metadata_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10))
tb.Label(metadata_frame, text="Document Number:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_number_var).grid(row=0, column=1, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Revision:").grid(row=0, column=2, padx=(10, 5), pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_rev_var).grid(row=0, column=3, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Project Name:").grid(row=1, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_project_var).grid(row=1, column=1, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Customer:").grid(row=1, column=2, padx=(10, 5), pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=customer_var).grid(row=1, column=3, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Security Class:").grid(row=2, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_security_var).grid(row=2, column=1, padx=5, pady=3, sticky="ew")
options_frame = tb.Frame(top_frame)
options_frame.grid(row=3, column=0, columnspan=3, pady=10, sticky="ew")
tb.Checkbutton(options_frame, text="Add Table of Contents", variable=add_toc_var, bootstyle="primary-round-toggle").pack(side=tk.LEFT, padx=(0, 20))
action_frame = tb.Frame(options_frame)
action_frame.pack(side=tk.LEFT)
tb.Button(action_frame, text="Convert to DOCX", command=lambda: convert("DOCX"), bootstyle=SUCCESS).pack(side=tk.LEFT, padx=5)
tb.Button(action_frame, text="Convert to PDF", command=lambda: convert("PDF"), bootstyle=SUCCESS).pack(side=tk.LEFT, padx=5)
tb.Button(action_frame, text="Open File", command=lambda: open_with_default_app(output_path.get()), bootstyle=WARNING).pack(side=tk.LEFT, padx=(20, 5))
tb.Button(action_frame, text="Open Folder", command=lambda: open_output_folder(output_path.get()), bootstyle=WARNING).pack(side=tk.LEFT, padx=5)
top_frame.columnconfigure(1, weight=1)
metadata_frame.columnconfigure(1, weight=1) metadata_frame.columnconfigure(1, weight=1)
metadata_frame.columnconfigure(3, weight=1) metadata_frame.columnconfigure(3, weight=1)
tb.Label(metadata_frame, text="Project Name:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_project_var).grid(row=0, column=1, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Customer:").grid(row=0, column=2, padx=(10, 5), pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=customer_var).grid(row=0, column=3, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Document Number:").grid(row=1, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_number_var).grid(row=1, column=1, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Revision:").grid(row=1, column=2, padx=(10, 5), pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_rev_var).grid(row=1, column=3, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Security Class:").grid(row=2, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_security_var).grid(row=2, column=1, padx=5, pady=3, sticky="ew")
options_frame = tb.Frame(container)
options_frame.pack(fill=tk.X, pady=(0, 10))
tb.Checkbutton(options_frame, text="Add Table of Contents", variable=add_toc_var, bootstyle="primary-round-toggle").pack(side=tk.LEFT, padx=(0, 20))
tb.Button(options_frame, text="Open Source Folder", command=lambda: open_output_folder(selected_file.get()), bootstyle=WARNING).pack(side=tk.LEFT, padx=5)
action_frame = tb.Labelframe(container, text="Conversion Actions", padding=10)
action_frame.pack(fill=tk.X, pady=10)
action_frame.columnconfigure(1, weight=1)
tb.Button(action_frame, text="MD -> DOCX", command=lambda: convert("DOCX"), bootstyle=SUCCESS, width=12).grid(row=0, column=0, padx=5, pady=5)
tb.Entry(action_frame, textvariable=docx_output_path).grid(row=0, column=1, padx=5, pady=5, sticky="ew")
tb.Button(action_frame, text="Open", command=lambda: open_with_default_app(docx_output_path.get()), bootstyle="secondary").grid(row=0, column=2, padx=5, pady=5)
tb.Button(action_frame, text="MD -> PDF", command=lambda: convert("PDF"), bootstyle=SUCCESS, width=12).grid(row=1, column=0, padx=5, pady=5)
tb.Entry(action_frame, textvariable=pdf_direct_output_path).grid(row=1, column=1, padx=5, pady=5, sticky="ew")
tb.Button(action_frame, text="Open", command=lambda: open_with_default_app(pdf_direct_output_path.get()), bootstyle="secondary").grid(row=1, column=2, padx=5, pady=5)
docx_to_pdf_btn = tb.Button(action_frame, text="DOCX -> PDF", command=convert_from_docx_to_pdf, bootstyle="info", width=12, state=tk.DISABLED)
docx_to_pdf_btn.grid(row=2, column=0, padx=5, pady=5)
tb.Entry(action_frame, textvariable=pdf_from_docx_output_path).grid(row=2, column=1, padx=5, pady=5, sticky="ew")
tb.Button(action_frame, text="Open", command=lambda: open_with_default_app(pdf_from_docx_output_path.get()), bootstyle="secondary").grid(row=2, column=2, padx=5, pady=5)
log_frame = tb.Labelframe(container, text="Log Viewer", padding=10) log_frame = tb.Labelframe(container, text="Log Viewer", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True, side=tk.BOTTOM) log_frame.pack(fill=tk.BOTH, expand=True)
log_text_widget = ScrolledText(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10) log_text_widget = ScrolledText(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10)
log_text_widget.pack(fill=tk.BOTH, expand=True) log_text_widget.pack(fill=tk.BOTH, expand=True)
add_tkinter_handler(gui_log_widget=log_text_widget, root_tk_instance_for_gui_handler=app, logging_config_dict=log_config) add_tkinter_handler(gui_log_widget=log_text_widget, root_tk_instance_for_gui_handler=app, logging_config_dict=log_config)
_update_output_paths()
if selected_template.get():
edit_template_btn.config(state=tk.NORMAL)
def on_closing(): def on_closing():
shutdown_logging_system() shutdown_logging_system()
app.destroy() app.destroy()