Advertisement
OpticalAlgorithm

Python HTML file merger to PDF

Dec 26th, 2024
143
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.24 KB | Source Code | 0 0
  1. import os
  2. import pdfkit
  3. import tkinter as tk
  4. from tkinter import ttk, filedialog, messagebox
  5. from pathlib import Path
  6. from typing import List
  7. from PyPDF2 import PdfMerger
  8. from bs4 import BeautifulSoup
  9. import threading
  10.  
  11.  
  12. class WebPageMergerGUI:
  13.     def __init__(self, root):
  14.         self.root = root
  15.         self.root.title("HTML to PDF Merger")
  16.         self.running = False
  17.         self.current_thread = None
  18.  
  19.         # Set minimum window size
  20.         self.root.minsize(600, 400)
  21.  
  22.         # Configure grid
  23.         self.root.grid_columnconfigure(0, weight=1)
  24.         self.root.grid_rowconfigure(2, weight=1)
  25.  
  26.         self.setup_gui()
  27.  
  28.     def setup_gui(self):
  29.         # Input directory selection
  30.         input_frame = ttk.Frame(self.root, padding="10")
  31.         input_frame.grid(row=0, column=0, sticky="ew")
  32.  
  33.         ttk.Label(input_frame, text="Input Directory:").pack(side=tk.LEFT)
  34.         self.input_path = tk.StringVar()
  35.         input_entry = ttk.Entry(input_frame, textvariable=self.input_path)
  36.         input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
  37.         ttk.Button(input_frame, text="Browse", command=self.browse_input).pack(side=tk.LEFT)
  38.  
  39.         # Output file selection
  40.         output_frame = ttk.Frame(self.root, padding="10")
  41.         output_frame.grid(row=1, column=0, sticky="ew")
  42.  
  43.         ttk.Label(output_frame, text="Output PDF:").pack(side=tk.LEFT)
  44.         self.output_path = tk.StringVar()
  45.         output_entry = ttk.Entry(output_frame, textvariable=self.output_path)
  46.         output_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
  47.         ttk.Button(output_frame, text="Browse", command=self.browse_output).pack(side=tk.LEFT)
  48.  
  49.         # Progress area
  50.         self.log_frame = ttk.Frame(self.root, padding="10")
  51.         self.log_frame.grid(row=2, column=0, sticky="nsew")
  52.  
  53.         # Create log text widget with scrollbar
  54.         self.log_text = tk.Text(self.log_frame, height=10, wrap=tk.WORD)
  55.         scrollbar = ttk.Scrollbar(self.log_frame, orient="vertical", command=self.log_text.yview)
  56.         self.log_text.configure(yscrollcommand=scrollbar.set)
  57.  
  58.         self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  59.         scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
  60.  
  61.         # Progress bar
  62.         self.progress_var = tk.DoubleVar()
  63.         self.progress_bar = ttk.Progressbar(
  64.             self.root,
  65.             variable=self.progress_var,
  66.             maximum=100
  67.         )
  68.         self.progress_bar.grid(row=3, column=0, sticky="ew", padx=10, pady=5)
  69.  
  70.         # Buttons frame
  71.         button_frame = ttk.Frame(self.root)
  72.         button_frame.grid(row=4, column=0, pady=10)
  73.  
  74.         # Convert button
  75.         self.convert_button = ttk.Button(
  76.             button_frame,
  77.             text="Convert to PDF",
  78.             command=self.start_conversion
  79.         )
  80.         self.convert_button.pack(side=tk.LEFT, padx=5)
  81.  
  82.         # Stop button
  83.         self.stop_button = ttk.Button(
  84.             button_frame,
  85.             text="Stop",
  86.             command=self.stop_conversion,
  87.             state=tk.DISABLED
  88.         )
  89.         self.stop_button.pack(side=tk.LEFT, padx=5)
  90.  
  91.     def browse_input(self):
  92.         directory = filedialog.askdirectory(title="Select Input Directory")
  93.         if directory:
  94.             self.input_path.set(directory)
  95.  
  96.     def browse_output(self):
  97.         file_path = filedialog.asksaveasfilename(
  98.             title="Save PDF As",
  99.             defaultextension=".pdf",
  100.             filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
  101.         )
  102.         if file_path:
  103.             self.output_path.set(file_path)
  104.  
  105.     def log(self, message):
  106.         self.log_text.insert(tk.END, message + "\n")
  107.         self.log_text.see(tk.END)
  108.         self.root.update_idletasks()
  109.  
  110.     def start_conversion(self):
  111.         if not self.input_path.get() or not self.output_path.get():
  112.             messagebox.showerror("Error", "Please select both input directory and output file location.")
  113.             return
  114.  
  115.         self.convert_button.configure(state="disabled")
  116.         self.stop_button.configure(state="normal")
  117.         self.running = True
  118.         self.progress_var.set(0)
  119.         self.log_text.delete(1.0, tk.END)
  120.  
  121.         # Start conversion in a separate thread
  122.         self.current_thread = threading.Thread(target=self.run_conversion)
  123.         self.current_thread.daemon = True
  124.         self.current_thread.start()
  125.  
  126.     def stop_conversion(self):
  127.         if self.running:
  128.             self.running = False
  129.             self.log("Stopping conversion process...")
  130.             self.stop_button.configure(state="disabled")
  131.  
  132.     def run_conversion(self):
  133.         try:
  134.             merger = WebPageMerger(
  135.                 self.input_path.get(),
  136.                 self.output_path.get(),
  137.                 self.log,
  138.                 self.update_progress,
  139.                 lambda: not self.running  # Stop check callback
  140.             )
  141.             merger.merge_to_pdf()
  142.             if self.running:  # Only show success if not stopped
  143.                 self.root.after(0, self.conversion_complete)
  144.             else:
  145.                 self.root.after(0, self.conversion_stopped)
  146.         except Exception as e:
  147.             self.root.after(0, lambda: self.conversion_error(str(e)))
  148.  
  149.     def conversion_complete(self):
  150.         messagebox.showinfo("Success", "PDF conversion completed successfully!")
  151.         self.reset_gui()
  152.  
  153.         # Open the PDF with default viewer
  154.         try:
  155.             pdf_path = self.output_path.get()
  156.             if os.name == 'nt':  # Windows
  157.                 os.startfile(pdf_path)
  158.             elif os.name == 'posix':  # Linux
  159.                 os.system(f'xdg-open "{pdf_path}"')
  160.             else:  # macOS
  161.                 os.system(f'open "{pdf_path}"')
  162.         except Exception as e:
  163.             messagebox.showerror("Error", f"Could not open PDF: {str(e)}")
  164.  
  165.     def conversion_stopped(self):
  166.         messagebox.showinfo("Stopped", "PDF conversion was stopped by user.")
  167.         self.reset_gui()
  168.  
  169.     def conversion_error(self, error_message):
  170.         messagebox.showerror("Error", f"An error occurred during conversion:\n{error_message}")
  171.         self.reset_gui()
  172.  
  173.     def reset_gui(self):
  174.         self.running = False
  175.         self.convert_button.configure(state="normal")
  176.         self.stop_button.configure(state="disabled")
  177.  
  178.     def update_progress(self, value):
  179.         self.progress_var.set(value)
  180.         self.root.update_idletasks()
  181.  
  182.  
  183. class WebPageMerger:
  184.     def __init__(self, input_dir: str, output_file: str, log_callback, progress_callback, stop_check):
  185.         self.input_dir = Path(input_dir).resolve()
  186.         self.output_file = Path(output_file).resolve()
  187.         self.temp_dir = Path('temp_html').resolve()
  188.         self.log = log_callback
  189.         self.update_progress = progress_callback
  190.         self.should_stop = stop_check
  191.  
  192.         # Create temp directory if it doesn't exist
  193.         self.temp_dir.mkdir(exist_ok=True)
  194.  
  195.     def _get_files(self) -> List[Path]:
  196.         files = sorted([f for f in self.input_dir.glob('*.html')])
  197.         self.log(f"Found {len(files)} HTML files.")
  198.         return files
  199.  
  200.     def _process_html(self, input_file: Path) -> Path:
  201.         """Process HTML file to make it self-contained with basic styling."""
  202.         try:
  203.             if self.should_stop():
  204.                 raise InterruptedError("Conversion stopped by user")
  205.  
  206.             # Read the original HTML
  207.             with open(input_file, 'r', encoding='utf-8') as f:
  208.                 soup = BeautifulSoup(f.read(), 'html.parser')
  209.  
  210.             # Remove all existing styles and scripts
  211.             for tag in soup.find_all(['style', 'script', 'link']):
  212.                 tag.decompose()
  213.  
  214.             # Add our own styling
  215.             style = soup.new_tag('style')
  216.             style.string = """
  217.                body {
  218.                    font-family: Arial, sans-serif;
  219.                    line-height: 1.6;
  220.                    margin: 40px;
  221.                    color: #333;
  222.                    max-width: 900px;
  223.                    margin: 40px auto;
  224.                }
  225.                h1, h2, h3 {
  226.                    color: #000;
  227.                    margin-top: 20px;
  228.                    margin-bottom: 10px;
  229.                    page-break-after: avoid;
  230.                }
  231.                p {
  232.                    margin-bottom: 10px;
  233.                }
  234.                table {
  235.                    border-collapse: collapse;
  236.                    margin: 15px 0;
  237.                    page-break-inside: avoid;
  238.                }
  239.                th, td {
  240.                    border: 1px solid #ddd;
  241.                    padding: 8px;
  242.                    text-align: left;
  243.                }
  244.                th {
  245.                    background-color: #f5f5f5;
  246.                }
  247.                a {
  248.                    color: #0645ad;
  249.                    text-decoration: none;
  250.                }
  251.                @media print {
  252.                    a {
  253.                        color: #000;
  254.                        text-decoration: none;
  255.                    }
  256.                }
  257.            """
  258.             soup.head.append(style)
  259.  
  260.             # Create temporary file
  261.             output_file = self.temp_dir / f"processed_{input_file.name}"
  262.             with open(output_file, 'w', encoding='utf-8') as f:
  263.                 f.write(str(soup))
  264.  
  265.             return output_file
  266.  
  267.         except Exception as e:
  268.             self.log(f"Error processing HTML {input_file.name}: {str(e)}")
  269.             raise
  270.  
  271.     def _convert_single_file(self, input_file: Path, output_file: Path) -> bool:
  272.         try:
  273.             if self.should_stop():
  274.                 raise InterruptedError("Conversion stopped by user")
  275.  
  276.             # Process the HTML file
  277.             processed_file = self._process_html(input_file)
  278.  
  279.             # Configure PDF options
  280.             options = {
  281.                 'page-size': 'Letter',
  282.                 'margin-top': '0.75in',
  283.                 'margin-right': '0.75in',
  284.                 'margin-bottom': '0.75in',
  285.                 'margin-left': '0.75in',
  286.                 'encoding': 'UTF-8',
  287.                 'enable-local-file-access': '',
  288.                 'quiet': '',
  289.                 'no-images': '',
  290.                 'disable-external-links': '',
  291.                 'disable-javascript': '',
  292.                 'print-media-type': ''
  293.             }
  294.  
  295.             # Convert to PDF
  296.             pdfkit.from_file(
  297.                 str(processed_file),
  298.                 str(output_file),
  299.                 options=options
  300.             )
  301.  
  302.             return output_file.exists() and output_file.stat().st_size > 0
  303.  
  304.         except Exception as e:
  305.             self.log(f"Error converting {input_file.name}: {str(e)}")
  306.             return False
  307.  
  308.     def merge_to_pdf(self):
  309.         try:
  310.             input_files = self._get_files()
  311.             if not input_files:
  312.                 raise ValueError(f"No HTML files found in {self.input_dir}")
  313.  
  314.             successful_pdfs = []
  315.             total_files = len(input_files)
  316.  
  317.             self.log("\nConverting files to PDF...")
  318.             for i, file in enumerate(input_files, 1):
  319.                 if self.should_stop():
  320.                     raise InterruptedError("Conversion stopped by user")
  321.  
  322.                 self.log(f"\nProcessing: {file.name}")
  323.                 output_pdf = self.temp_dir / f"{file.stem}.pdf"
  324.  
  325.                 if self._convert_single_file(file, output_pdf):
  326.                     successful_pdfs.append(output_pdf)
  327.                     self.log(f"Successfully converted: {file.name}")
  328.                 else:
  329.                     self.log(f"Failed to convert: {file.name}")
  330.  
  331.                 # Update progress bar
  332.                 progress = (i / total_files) * 100
  333.                 self.update_progress(progress)
  334.  
  335.             if not successful_pdfs:
  336.                 raise ValueError("No files were successfully converted to PDF")
  337.  
  338.             if self.should_stop():
  339.                 raise InterruptedError("Conversion stopped by user")
  340.  
  341.             self.log(f"\nMerging {len(successful_pdfs)} PDFs...")
  342.             merger = PdfMerger()
  343.             for pdf in successful_pdfs:
  344.                 if pdf.exists() and pdf.stat().st_size > 0:
  345.                     merger.append(str(pdf))
  346.  
  347.             merger.write(str(self.output_file))
  348.             merger.close()
  349.  
  350.             self.log(f"\nSuccessfully created final PDF: {self.output_file}")
  351.             self.log(f"Total files converted: {len(successful_pdfs)}/{len(input_files)}")
  352.  
  353.         except InterruptedError:
  354.             self.log("\nConversion process stopped by user.")
  355.             raise
  356.  
  357.         except Exception as e:
  358.             self.log(f"Error in merge process: {str(e)}")
  359.             raise
  360.  
  361.         finally:
  362.             self.log("\nCleaning up temporary files...")
  363.             if self.temp_dir.exists():
  364.                 for temp_file in self.temp_dir.glob('*'):
  365.                     try:
  366.                         temp_file.unlink()
  367.                     except Exception:
  368.                         pass
  369.                 try:
  370.                     self.temp_dir.rmdir()
  371.                 except Exception:
  372.                     pass
  373.  
  374.  
  375. def main():
  376.     root = tk.Tk()
  377.     app = WebPageMergerGUI(root)
  378.     root.mainloop()
  379.  
  380.  
  381. if __name__ == '__main__':
  382.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement