From c6ad73d4492ed539b2c1246866a95befe645e8e5 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 18 Jun 2025 08:09:24 +0200 Subject: [PATCH] review gui, add convert docx to pdf --- .~lock.TemplateSumSample.docx# | 1 + TemplateSumSample.docx | Bin 52241 -> 44352 bytes markdownconverter/core/core.py | 405 +++++++++++++++++++++++++-------- markdownconverter/gui/gui.py | 208 +++++++++++++---- 4 files changed, 471 insertions(+), 143 deletions(-) create mode 100644 .~lock.TemplateSumSample.docx# diff --git a/.~lock.TemplateSumSample.docx# b/.~lock.TemplateSumSample.docx# new file mode 100644 index 0000000..ccbbcdd --- /dev/null +++ b/.~lock.TemplateSumSample.docx# @@ -0,0 +1 @@ +Luca Vallongo,Win11_Dev/admin,Win11_Dev,18.06.2025 08:03,file:///C:/Users/admin/AppData/Roaming/LibreOffice/4; \ No newline at end of file diff --git a/TemplateSumSample.docx b/TemplateSumSample.docx index 8a92aa3f6360c4532aeca365e2d473bf3e3d4893..1a06bccbcbceb851c7779ed4f1cac91d1e065867 100644 GIT binary patch delta 11798 zcmZvCWk6j!vo`MTR;;*FoZ{~8?k>fl$i|BM#@&k*_oBt2xVyW1@sHB?zW4Neckdr- zWim5)R%VjPB#BK1?^pmsRFDRTzyN`PfdM&Xn5aS|1_gh80+~R`0J8F%Oen3V6_sA{ z8MP42YSv^}BqV-;we!dhEF&?gE(AlaRLlD9^^3_r9M{-4Juc05hTUDBCupw_W(EW+ z9N1`?!RUk34(uWUtm-{z?{}N*n($c5&XEZC%=OiNC@^a@)&zhal5=8&)aLytJMZ-r zhIKFk=XI;`9V7Y037E@#SX+p~VR$}6>TjWwmqgBNSaL#xg$LrE`&DD6Ohl-W`>UZS zj0N08snmqE81c@ncqhk@iudc?XZRuKC^O~hr~m9YcKx`>fW~^IAG_MXu{v4wWlE6- zH2E7+q=KQJr-dSM_JgDH*6hZjsneJFmh@Rs4=ojQ*}7~>HUNF;1Exn35$x@a!Tc~4 z=ly`{oCJr6MU;pNyqt!mmj;z|CkJw;x{jN%LYAby4hL*p)`lLY+cUdPM_%BK(QVtp zi!b%=h}1i6Uc#b6!@|!VS(g+FSVjCO-eNhX%9-QMg_jlLaIz4^$nc{dGC!0asC;;E z_{71-<9Ft%TCIR?!`KQoBVezBg8$_~I4J;U)8o9I1YZGMXeb*zbcQ1J023`i^f@M@ z|2D8VprJkR30y%M8s;H}D4`k@1jGRx1mrK{0^fnZ2MpOQGNH8G(n93SDj*NWp^qVi zVemp)fRsAe_Xg=`(vigxr%XLRly7#AHSr9eAeblWH~`YW9w7>|j@VsaoHXj z0$&aQOJ$dI6qR@s<_^Ftrk&f1aCf*7V+kl@F(h`o3ubbFI)-8-r?82LwhvN>M(*G(?zo$k`P@h^1fFe@iKgKS^HjJ`FP5s?blKk&z<`Ja?6@TZA z%L1BO2F)SuoY!!DVv1CzkM8u6cz1ZI6!~$F_E__V5OKwFkmK zM_bJhggwODaPy1B0xH820#f3TNNR^uAU+5z^hH&go)z1An55|VJjK!tup4wZWXozV z;2LW#$bT=Dr?oux`t@tia}aH#UsO<#mj_I`Ic2eHlRy7m`#%3)C#C4hy zr@3G15$Hoh_-Wf|TZ!DW)rsoO>~NjtH^^NA(x!Rp2q<;MKiv z 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 W3;8-0KtL#8{~zH%Ku&Xim;N85UUFst delta 18558 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 P^!hLIYNoYx{!aZrj%zP} diff --git a/markdownconverter/core/core.py b/markdownconverter/core/core.py index 4167a30..c53724d 100644 --- a/markdownconverter/core/core.py +++ b/markdownconverter/core/core.py @@ -8,10 +8,23 @@ import docx import pypandoc import pdfkit 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 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 --- try: config = pdfkit.configuration() @@ -23,25 +36,38 @@ except OSError: log.info(f"pdfkit configured using fallback path: {WKHTMLTOPDF_PATH}") else: 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): - match = re.search(r"^\s*#\s+(.+)", markdown_text, re.MULTILINE) - return match.group(1).strip() if match else "Untitled Document" +def _get_document_title(markdown_text: str) -> str: + """Extracts the first heading (any level) from the markdown text to use as the title.""" + 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) match = pattern.search(markdown_text) + if not match: log.warning(f"'{separator_heading}' section not found. No revision history will be added.") return "", markdown_text + 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 -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) if not any(key in full_text for key in placeholders): return @@ -69,7 +95,8 @@ def _replace_text_in_paragraph(paragraph, placeholders): if font.color and 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: _replace_text_in_paragraph(p, placeholders) for table in element.tables: @@ -77,30 +104,292 @@ def _replace_text_in_element(element, placeholders): for cell in row.cells: _replace_text_in_element(cell, placeholders) -def _replace_text_placeholders(doc, placeholders): - log.info(f"Replacing text placeholders: {list(placeholders.keys())}") +def _replace_metadata_placeholders(doc: docx.Document, placeholders: dict[str, str]): + """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) for section in doc.sections: _replace_text_in_element(section.header, 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: if placeholder in "".join(run.text for run in p.runs): return p 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() index = parent.index(paragraph._p) + source_doc = docx.Document(source_docx_path) + for element in source_doc.element.body: parent.insert(index, element) index += 1 + parent.remove(paragraph._p) -# --- Main Conversion Function --- -def convert_markdown(input_file, output_format, add_toc=False, template_path=None, metadata=None): +def _remove_paragraph(paragraph): + """Removes a paragraph from its parent element.""" + if paragraph is None: + return + parent = paragraph._p.getparent() + parent.remove(paragraph._p) + +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 + + if not rev_history_md: + log.info("No revision history content found. Removing placeholder.") + _remove_paragraph(placeholder_p) + return + + lines = [line.strip() for line in rev_history_md.strip().split('\n')] + + 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.") + if config is None: + raise FileNotFoundError( + "wkhtmltopdf executable not found. Cannot create PDF." + ) + + title = _get_document_title(markdown_text) + + content_without_title = 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 = "" + if add_toc and hasattr(md_converter, 'toc') and md_converter.toc: + log.info("Generating Table of Contents for PDF.") + toc_html = f"

Table of Contents

{md_converter.toc}
" + + full_html = f""" + + + + + {title} + + + +

{title}

+ {toc_html} + {html_body} + + + """ + + 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}") + + +def _convert_to_docx(markdown_text: str, output_file: str, template_path: str, metadata: dict, add_toc: bool): + """Converts markdown text to a DOCX file using a template.""" + 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) + + # --- 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) + placeholders = {f"%%{key}%%": value for key, value in metadata.items() if value} + placeholders["%%DOC_DATE%%"] = date.today().strftime("%d/%m/%Y") + _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) + + # --- Step 4: Add Revision History Table Natively --- + _add_revision_table(doc, rev_history_md) + + temp_files = [] + try: + # --- Step 5: Insert Main Content and TOC with Page Breaks --- + if main_content_md: + content_for_pandoc = main_content_md + match = re.search(r"^\s*#\s+(.+)\n?", content_for_pandoc, re.MULTILINE) + if match: + log.info("Removing main title from content to exclude it from DOCX TOC.") + content_for_pandoc = content_for_pandoc[match.end():] + + log.info("Stripping manual numbering from headings.") + content_for_pandoc = re.sub( + r"^(\s*#+)\s+[0-9\.]+\s+", + r"\1 ", + content_for_pandoc, + flags=re.MULTILINE + ) + + 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) + + _remove_paragraph(toc_placeholder_p) + else: + _remove_paragraph(toc_placeholder_p) + _remove_paragraph(content_placeholder_p) + + doc.save(output_file) + log.info(f"Document successfully created at {output_file}") + + finally: + # --- Final Step: Cleanup --- + for temp_file in temp_files: + if os.path.exists(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}") @@ -108,90 +397,16 @@ def convert_markdown(input_file, output_format, add_toc=False, template_path=Non with open(input_file, 'r', encoding='utf-8') as f: markdown_text = f.read() - - # --- CORREZIONE LOGICA PDF --- + if output_format == "PDF": - output_file = os.path.splitext(input_file)[0] + ".pdf" - log.info("Starting PDF conversion using pdfkit.") - if config is None: - raise FileNotFoundError("wkhtmltopdf not found.") - - md_converter = markdown.Markdown(extensions=['toc', 'fenced_code', 'tables']) - - # Estrai il titolo dal testo markdown - title = _get_document_title(markdown_text) - - # Converti il corpo del testo - html_body = md_converter.convert(markdown_text) - - toc_html = "" - # Genera il TOC se richiesto - if add_toc and hasattr(md_converter, 'toc') and md_converter.toc: - log.info("Generating Table of Contents for PDF.") - # Mettiamo il TOC dopo il titolo principale, con un page-break - toc_html = f"

Table of Contents

{md_converter.toc}
" - - # Costruisci l'HTML finale, usando il titolo estratto sia nel che come <h1> - full_html = f""" - <!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="UTF-8"> - <title>{title} - - -

{title}

- {toc_html} - {html_body} - - - """ - - pdfkit.from_string(full_html, output_file, configuration=config, options={'encoding': "UTF-8"}) - log.info(f"PDF successfully generated: {output_file}") + _convert_to_pdf(markdown_text, output_path, add_toc) elif output_format == "DOCX": - output_file = os.path.splitext(input_file)[0] + ".docx" - if not template_path: - raise FileNotFoundError("A DOCX template file is required.") + if metadata is None: + metadata = {} + _convert_to_docx(markdown_text, output_path, template_path, metadata, add_toc) - doc = docx.Document(template_path) - - if metadata: - 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["%%DOC_DATE%%"] = date.today().strftime("%d/%m/%Y") - _replace_text_placeholders(doc, placeholders) - - rev_history_md, main_content_md = _split_markdown_by_revision_history(markdown_text) - - temp_files = [] - try: - rev_placeholder_p = _find_placeholder_paragraph(doc, "%%REVISION_RECORD%%") - if rev_history_md and rev_placeholder_p: - with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as temp_file: - pypandoc.convert_text(rev_history_md, 'docx', format='md', outputfile=temp_file.name) - temp_files.append(temp_file.name) - _insert_docx_at_paragraph(rev_placeholder_p, temp_file.name) - - content_placeholder_p = _find_placeholder_paragraph(doc, "%%DOC_CONTENT%%") - if main_content_md and content_placeholder_p: - pandoc_args = ["--toc"] if add_toc else [] - with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as temp_file: - pypandoc.convert_text(main_content_md, 'docx', format='md', extra_args=pandoc_args, outputfile=temp_file.name) - temp_files.append(temp_file.name) - - content_placeholder_p.insert_paragraph_before().add_run().add_break(docx.enum.text.WD_BREAK.PAGE) - _insert_docx_at_paragraph(content_placeholder_p, temp_file.name) - - doc.save(output_file) - log.info(f"Document successfully created at {output_file}") - - finally: - for temp_file in temp_files: - if os.path.exists(temp_file): - os.remove(temp_file) else: raise ValueError(f"Unsupported output format: {output_format}") - return output_file \ No newline at end of file + return output_path \ No newline at end of file diff --git a/markdownconverter/gui/gui.py b/markdownconverter/gui/gui.py index 20450b5..b248b0b 100644 --- a/markdownconverter/gui/gui.py +++ b/markdownconverter/gui/gui.py @@ -5,12 +5,13 @@ import sys import logging import subprocess import tkinter as tk +from datetime import date import ttkbootstrap as tb from tkinter.scrolledtext import ScrolledText from ttkbootstrap.constants import * 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.logger import ( setup_basic_logging, @@ -23,8 +24,8 @@ from .editor import EditorWindow log = get_logger(__name__) def open_with_default_app(filepath): - if not filepath: - log.warning("Open file/folder requested, but no path was provided.") + if not filepath or not os.path.exists(filepath): + log.warning("Open file/folder requested, but path is invalid or not provided.") messagebox.showwarning("Warning", "No output file or folder to open.") return try: @@ -51,7 +52,7 @@ def open_output_folder(filepath): def run_app(): app = tb.Window(themename="sandstone") app.title("Markdown Converter") - app.geometry("800x750") + app.geometry("900x850") app.resizable(True, True) log_config = { @@ -70,17 +71,56 @@ def run_app(): config = load_configuration() metadata_config = config.get("metadata", {}) + # --- Variabili di stato della GUI --- selected_file = StringVar(value=config.get("last_markdown_file", "")) selected_template = StringVar(value=config.get("last_template_file", "")) add_toc_var = BooleanVar(value=config.get("add_toc", True)) - output_path = StringVar() - + doc_security_var = StringVar(value=metadata_config.get("DOC_SECURITY", "")) doc_number_var = StringVar(value=metadata_config.get("DOC_NUMBER", "")) doc_rev_var = StringVar(value=metadata_config.get("DOC_REV", "")) 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(): file_path = selected_file.get() if not file_path or not os.path.exists(file_path): @@ -94,17 +134,48 @@ def run_app(): selected_file.set(path) def browse_template(): - # --- CORREZIONE: Cerca prima i file .docx --- path = filedialog.askopenfilename( title="Select a Template Document", filetypes=[("Word Documents", "*.docx"), ("All files", "*.*")] ) if 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): - file_path = selected_file.get() - if not file_path: + nonlocal docx_to_pdf_btn + 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.") return @@ -113,6 +184,19 @@ def run_app(): messagebox.showwarning("Warning", "A DOCX template is required.") 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 = { 'DOC_SECURITY': doc_security_var.get(), 'DOC_NUMBER': doc_number_var.get(), @@ -123,71 +207,99 @@ def run_app(): try: output = convert_markdown( - input_file=file_path, output_format=fmt, add_toc=add_toc_var.get(), - template_path=template, metadata=metadata_to_pass + input_file=input_path, + 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}") 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 ) + + if fmt == "DOCX" and docx_to_pdf_btn: + docx_to_pdf_btn.config(state=tk.NORMAL) + except Exception as e: log.critical(f"An error occurred during conversion: {e}", exc_info=True) messagebox.showerror("Error", f"An error occurred during conversion:\n{str(e)}") + # --- Costruzione della GUI --- container = tb.Frame(app, padding=10) 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") - tb.Entry(top_frame, textvariable=selected_file, width=70).grid(row=0, column=1, padx=5, sticky="ew") - file_button_frame = tb.Frame(top_frame) + input_frame = tb.Labelframe(container, text="Input Files", padding=10) + input_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10)) + 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") 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.Label(top_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.Button(top_frame, text="Browse...", command=browse_template, bootstyle=SECONDARY).grid(row=1, column=2, padx=5) + tb.Label(input_frame, text="Template File:").grid(row=1, column=0, padx=(0, 10), pady=5, sticky="w") + tb.Entry(input_frame, textvariable=selected_template).grid(row=1, column=1, padx=5, pady=5, sticky="ew") + 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.grid(row=2, column=0, columnspan=3, padx=0, pady=10, sticky="ew") - - 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 = tb.Labelframe(container, text="Template Placeholders", padding=10) + metadata_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10)) metadata_frame.columnconfigure(1, 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.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.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) + _update_output_paths() + if selected_template.get(): + edit_template_btn.config(state=tk.NORMAL) + def on_closing(): shutdown_logging_system() app.destroy()