admin 发表于 2026-10-15 19:27:26

txt转批量pdf(用到了阿里巴巴普惠体3.0)

txt转批量pdf(用到了阿里巴巴普惠体3.0)

纯文本查看 复制代码import tkinter
import tkinter.filedialog
import tkinter.messagebox
import customtkinter
import os
import multiprocessing # Changed from threading
from multiprocessing import Manager, Pool # Use Manager for shared Queue, Pool directly
import queue # Added for queue.Empty exception
# Removed fpdf import
# --- ReportLab Imports ---
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# --- End ReportLab Imports ---
from pathlib import Path
import time
import locale
import math
# Removed datetime and timedelta imports

FONT_PATH = 'AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.ttf'
FONT_NAME = 'AlibabaPuHuiTiR' # ReportLab font name (can be different)
# Use CPU count for true parallelism, fallback to 4 if None
MAX_CONCURRENT = os.cpu_count() if os.cpu_count() else 4
BUFFER_SIZE = 16 * 1024 * 1024# 16MB 缓冲区大小

customtkinter.set_appearance_mode("System")
customtkinter.set_default_color_theme("blue")

# --- Font Registration (Do this once, ideally before multiprocessing starts, but needs path) ---
# It's generally safer to register in the worker process if path issues arise,
# but let's try registering globally first. If it causes issues (e.g., font not found in worker),
# we'll move it into the worker.
try:
    if os.path.exists(FONT_PATH):
      pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
    else:
      # This error should ideally be caught before starting the GUI
      print(f" Font file not found during initial registration: {FONT_PATH}")
      # We'll still raise an error in the worker if needed.
except Exception as e:
    print(f" Failed to register font globally: {e}")


# --- Worker Functions (Top Level for Multiprocessing) ---
def convert_file_worker(txt_path, output_queue):
    """Worker function executed in a separate process using ReportLab."""
    pdf_path = Path(txt_path).with_suffix('.pdf')
    file_basename = os.path.basename(txt_path)
    content = None # Initialize content

    try:
      # 尝试读取文件内容和检测编码
      encodings_to_try = ['utf-8', 'gbk', locale.getpreferredencoding(False)]

      for enc in encodings_to_try:
            try:
                with open(txt_path, 'rb', buffering=BUFFER_SIZE) as f:
                     raw_content = f.read()
                content = raw_content.decode(enc)
                break
            except UnicodeDecodeError:
                continue
            except Exception as read_err:
                raise Exception(f"读取时出错 ({txt_path}): {read_err}")

      if content is None:
            raise Exception(f"无法使用 {encodings_to_try} 解码文件: {file_basename}")

      if not content.strip():
            output_queue.put(("status", txt_path, True, "文件为空,已跳过"))
            return

      # --- ReportLab PDF Generation ---
      try:
            # Check font registration within worker (more robust)
            # This might be slightly redundant if global registration worked, but safer.
            if FONT_NAME not in pdfmetrics.getRegisteredFontNames():
               if os.path.exists(FONT_PATH):
                      pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
               else:
                      raise Exception(f"错误:字体文件 '{FONT_PATH}' 在工作进程中未找到!")

            doc = SimpleDocTemplate(str(pdf_path), pagesize=A4) # Use str(pdf_path)
            styles = getSampleStyleSheet()
            
            # Create a custom style using the registered font
            custom_style = ParagraphStyle(
                name='CustomStyle',
                parent=styles['Normal'],
                fontName=FONT_NAME,
                fontSize=10,
                leading=14, # Line spacing
                encoding='utf8' # Important for ReportLab with Unicode
            )

            # Prepare content for ReportLab Flowables
            # Replace potential problematic characters if needed, handle newlines
            # ReportLab's Paragraph handles HTML-like tags for basic formatting
            # We'll treat each line as a paragraph for simplicity, preserving line breaks.
            # Replace \r\n with \n, then split by \n
            lines = content.replace('\r\n', '\n').split('\n')
            story = []
            for line in lines:
                # Escape HTML tags within the line to prevent misinterpretation
                # escaped_line = line.replace('&', '&').replace('<', '<').replace('>', '>')
                # Or simply use as is if no HTML-like tags are expected in TXT
                para = Paragraph(line, custom_style)
                story.append(para)
                # Add a small spacer for visual separation if desired, or rely on leading
                # story.append(Spacer(1, 2))

            # Build the PDF
            doc.build(story)

      except Exception as pdf_err:
             raise Exception(f"使用 ReportLab 生成 PDF 时出错 ({file_basename}): {pdf_err}")
      # --- End ReportLab PDF Generation ---

      output_queue.put(("status", txt_path, True, f"成功转换为 {os.path.basename(pdf_path)}"))

    except Exception as e:
      output_queue.put(("status", txt_path, False, f"处理 {file_basename} 时出错: {e}"))

# 将转换进程目标函数移到类外部
def start_conversion_process_target(file_list, queue_ref):
    """Target function for the management process."""
    try:
      with Pool(processes=MAX_CONCURRENT) as pool:
            for txt_file in file_list:
                pool.apply_async(convert_file_worker, args=(txt_file, queue_ref))
            pool.close()
            pool.join()
      queue_ref.put(("done",))
    except Exception as e:
      queue_ref.put(("error", f"转换管理进程出错: {e}"))

# --- Main Application Class ---
class App(customtkinter.CTk):
    def __init__(self):
      super().__init__()

      self.title("TXT to PDF 批量转换器 (ReportLab)") # Updated title
      self.geometry("700x550")
      self.configure(fg_color=("#f5f5f5", "#2b2b2b"))

      self.grid_columnconfigure(0, weight=1)
      self.grid_rowconfigure(3, weight=1)

      main_frame = customtkinter.CTkFrame(self, corner_radius=15, fg_color=("white", "#333333"))
      main_frame.grid(row=0, column=0, padx=30, pady=30, sticky="nsew")
      main_frame.grid_columnconfigure(0, weight=1)
      main_frame.grid_columnconfigure(1, weight=0)
      main_frame.grid_rowconfigure(3, weight=1)

      title_label = customtkinter.CTkLabel(
            main_frame,
            text="TXT to PDF 批量转换器",
            font=customtkinter.CTkFont(size=24, weight="bold"),
            text_color=("#1a1a1a", "#ffffff")
      )
      title_label.grid(row=0, column=0, columnspan=2, padx=20, pady=(20, 30))

      self.select_folder_button = customtkinter.CTkButton(
            main_frame,
            text="选择 TXT 文件",
            command=self.select_folder,
            height=40,
            corner_radius=8,
            font=customtkinter.CTkFont(size=14),
            fg_color=("#2986cc", "#1f6aa5"),
            hover_color=("#246ea6", "#195785")
      )
      self.select_folder_button.grid(row=1, column=0, padx=(30, 5), pady=(0, 15), sticky="ew")

      self.start_button = customtkinter.CTkButton(
            main_frame,
            text="开始转换",
            command=self.start_conversion,
            state="disabled",
            height=40,
            corner_radius=8,
            font=customtkinter.CTkFont(size=14),
            fg_color=("#27ae60", "#219653"),
            hover_color=("#219653", "#1e8449")
      )
      self.start_button.grid(row=1, column=1, padx=(5, 30), pady=(0, 15), sticky="ew")

      self.folder_path_label = customtkinter.CTkLabel(
            main_frame,
            text="未选择文件",
            font=customtkinter.CTkFont(size=12),
            text_color=("#666666", "#999999"),
            anchor="w"
      )
      self.folder_path_label.grid(row=2, column=0, columnspan=2, padx=30, pady=(0, 15), sticky="ew")

      self.status_textbox = customtkinter.CTkTextbox(
            main_frame,
            state="disabled",
            wrap="word",
            height=200,
            corner_radius=8,
            border_width=1,
            border_color=("#e0e0e0", "#404040"),
            fg_color=("#ffffff", "#2b2b2b")
      )
      self.status_textbox.grid(row=3, column=0, columnspan=2, padx=30, pady=(0, 20), sticky="nsew")

      self.progressbar = customtkinter.CTkProgressBar(
            main_frame,
            height=15,
            corner_radius=5,
            fg_color=("#f0f0f0", "#333333"),
            progress_color=("#2986cc", "#1f6aa5")
      )
      self.progressbar.grid(row=4, column=0, columnspan=2, padx=30, pady=(0, 10), sticky="ew")
      self.progressbar.set(0)

      self.progress_percent_label = customtkinter.CTkLabel(
            main_frame,
            text="0%",
            font=customtkinter.CTkFont(size=12),
            text_color=("#666666", "#999999")
      )
      self.progress_percent_label.grid(row=5, column=0, columnspan=2, padx=30, pady=(0, 20), sticky="w")

      self.selected_folder = ""
      self.txt_files = []
      self.manager = None
      self.conversion_queue = None
      self.conversion_process = None
      self.monitor_queue_id = None

      self.start_time = None
      self.processed_files = 0
      self.total_files = 0
      self.failed_files = []

    def select_folder(self):
      if self.conversion_process and self.conversion_process.is_alive():
            self.log_status("请等待当前转换完成。")
            return

      files = tkinter.filedialog.askopenfilenames(filetypes=[("TXT files", "*.txt")])
      if files:
            self.txt_files = list(files)
            self.selected_folder = os.path.dirname(self.txt_files)
            self.folder_path_label.configure(text=f"已选择 {len(self.txt_files)} 个文件")
            self.log_status(f"已选择 {len(self.txt_files)} 个 TXT 文件")
            self.start_button.configure(state="normal")
            self.update_progress(value=0)
            self.processed_files = 0
            self.total_files = len(self.txt_files)
            self.failed_files = []
      else:
            if self.txt_files:
               self.selected_folder = ""
               self.txt_files = []
               self.folder_path_label.configure(text="未选择文件")
               self.start_button.configure(state="disabled")
               self.log_status("未选择文件。")
               self.update_progress(value=0)

    def log_status(self, message):
      def _update():
            self.status_textbox.configure(state="normal")
            self.status_textbox.insert("end", f"{message}\n")
            self.status_textbox.configure(state="disabled")
            self.status_textbox.see("end")
      self.after(0, _update)

    def update_progress(self, value=None):
      def _update():
            if value is not None:
                self.progressbar.set(value)
                percent = int(value * 100)
                self.progress_percent_label.configure(text=f"{percent}%")
      self.after(0, _update)

    def process_queue(self):
      try:
            while True:
                message = self.conversion_queue.get_nowait()
                msg_type = message

                if msg_type == "status":
                  _, txt_path, success, status_msg = message
                  self.processed_files += 1
                  progress_value = self.processed_files / self.total_files if self.total_files > 0 else 0
                  self.update_progress(value=progress_value)

                  log_prefix = "[成功]" if success else "[失败]"
                  self.log_status(f"{log_prefix} {os.path.basename(txt_path)}: {status_msg}")
                  if not success:
                        self.failed_files.append(os.path.basename(txt_path))

                elif msg_type == "done":
                  end_time = time.time()
                  duration = end_time - self.start_time if self.start_time else 0
                  self.log_status("-" * 20)
                  self.log_status(f"转换完成!总共处理 {self.processed_files} 个文件,耗时: {duration:.2f} 秒。")
                  if self.failed_files:
                        self.log_status(f"失败 {len(self.failed_files)} 个文件: {', '.join(self.failed_files)}")
                  else:
                        self.log_status("所有文件转换成功!")
                  self.start_button.configure(state="normal")
                  self.select_folder_button.configure(state="normal")
                  self.conversion_process = None
                  self.monitor_queue_id = None
                  self.update_progress(value=1.0)
                  if self.manager:
                        try:
                            self.manager.shutdown()
                        except Exception as e:
                            print(f"Error shutting down manager after completion: {e}")
                        self.manager = None
                  return

                elif msg_type == "error":
                  self.log_status(f"[严重错误] {message}")
                  self.start_button.configure(state="normal")
                  self.select_folder_button.configure(state="normal")
                  self.conversion_process = None
                  self.monitor_queue_id = None
                  if self.manager:
                        try:
                            self.manager.shutdown()
                        except Exception as e:
                            print(f"Error shutting down manager on error: {e}")
                        self.manager = None
                  return

      except queue.Empty:
            pass

      if self.conversion_process and self.conversion_process.is_alive():
             self.monitor_queue_id = self.after(100, self.process_queue)
      elif self.processed_files < self.total_files and self.conversion_process and not self.conversion_process.is_alive():
             self.log_status("[错误] 转换进程意外终止。")
             self.start_button.configure(state="normal")
             self.select_folder_button.configure(state="normal")
             self.conversion_process = None
             self.monitor_queue_id = None
             if self.manager:
               try:
                     self.manager.shutdown()
               except Exception as e:
                     print(f"Error shutting down manager on unexpected process exit: {e}")
               self.manager = None
      elif self.processed_files == self.total_files and self.conversion_queue and not self.conversion_queue.empty():
             self.monitor_queue_id = self.after(100, self.process_queue)


    def start_conversion(self):
      if not self.txt_files:
            self.log_status("请先选择 TXT 文件。")
            return

      if self.conversion_process and self.conversion_process.is_alive():
             self.log_status("转换已经在进行中...")
             return

      if self.monitor_queue_id:
            self.after_cancel(self.monitor_queue_id)
            self.monitor_queue_id = None

      if self.conversion_process:
            if self.conversion_process.is_alive():
                self.conversion_process.terminate()
                self.conversion_process.join(timeout=0.5)
            self.conversion_process = None
      if self.manager:
             try:
               self.manager.shutdown()
             except Exception as e:
               print(f"Error shutting down old manager: {e}")
             self.manager = None

      self.manager = Manager()
      self.conversion_queue = self.manager.Queue()

      self.start_button.configure(state="disabled")
      self.select_folder_button.configure(state="disabled")
      self.status_textbox.configure(state="normal")
      self.status_textbox.delete("1.0", "end")
      self.status_textbox.configure(state="disabled")

      self.update_progress(value=0)
      self.processed_files = 0
      self.total_files = len(self.txt_files)
      self.failed_files = []

      self.log_status(f"开始使用 {MAX_CONCURRENT} 个进程转换 {self.total_files} 个文件 (使用 ReportLab)...") # Updated log
      self.start_time = time.time()

      self.conversion_process = multiprocessing.Process(
            target=start_conversion_process_target,
            args=(self.txt_files, self.conversion_queue),
            daemon=False
      )
      self.conversion_process.start()

      self.monitor_queue_id = self.after(100, self.process_queue)

    def on_closing(self):
      if self.conversion_process and self.conversion_process.is_alive():
            if tkinter.messagebox.askyesno("退出", "转换仍在进行中,确定要退出吗?"):
                print("Terminating conversion process...")
                self.conversion_process.terminate()
                self.conversion_process.join(timeout=0.5)
                if self.manager:
                  try:
                        self.manager.shutdown()
                  except Exception as e:
                        print(f"Error shutting down manager during termination: {e}")
                self.destroy()
            else:
                return
      else:
            if self.manager:
               try:
                     self.manager.shutdown()
               except Exception as e:
                     print(f"Error shutting down manager on normal close: {e}")
            self.destroy()

if __name__ == "__main__":
    multiprocessing.freeze_support()

    # Font check now includes trying to register it early
    font_ok = False
    if os.path.exists(FONT_PATH):
      try:
            pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
            font_ok = True
            print(f"Font '{FONT_NAME}' registered successfully from {FONT_PATH}")
      except Exception as e:
            print(f" Failed to register font '{FONT_NAME}' from {FONT_PATH}: {e}")
    else:
      print(f" Font file not found: {FONT_PATH}")

    if not font_ok:
      root = tkinter.Tk()
      root.withdraw()
      tkinter.messagebox.showerror("字体错误", f"错误:无法加载或注册字体文件 '{FONT_PATH}'! 请确保文件存在且有效。")
      root.destroy()
      exit()

    try:
      app = App()
      app.protocol("WM_DELETE_WINDOW", app.on_closing)
      app.mainloop()
    except Exception as e:
      print(f"Application failed to start: {e}")
      root = tkinter.Tk()
      root.withdraw()
      tkinter.messagebox.showerror("启动错误", f"无法启动应用程序: {e}")
      root.destroy()
页: [1]
查看完整版本: txt转批量pdf(用到了阿里巴巴普惠体3.0)