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 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,67 +104,126 @@ 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):
if not os.path.exists(input_file):
raise FileNotFoundError(f"Input file not found: {input_file}")
def _remove_paragraph(paragraph):
"""Removes a paragraph from its parent element."""
if paragraph is None:
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:
markdown_text = f.read()
if not rev_history_md:
log.info("No revision history content found. Removing placeholder.")
_remove_paragraph(placeholder_p)
return
# --- CORREZIONE LOGICA PDF ---
if output_format == "PDF":
output_file = os.path.splitext(input_file)[0] + ".pdf"
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 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)
# Converti il corpo del testo
html_body = md_converter.convert(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 = ""
# 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"<div style='page-break-after: always;'><h2>Table of Contents</h2>{md_converter.toc}</div>"
toc_html = f"<h2>Table of Contents</h2>{md_converter.toc}<div style='page-break-after: always;'></div>"
# Costruisci l'HTML finale, usando il titolo estratto sia nel <title> che come <h1>
full_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>
body {{ font-family: sans-serif; }}
h1, h2 {{ border-bottom: 1px solid #eaecef; padding-bottom: .3em; }}
</style>
</head>
<body>
<h1>{title}</h1>
@ -147,51 +233,180 @@ def convert_markdown(input_file, output_format, add_toc=False, template_path=Non
</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}")
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.")
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)
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)
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)
_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:
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)
# --- 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():]
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)
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
)
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)
_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}")
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:
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 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()