21 August, 2025

[Python] Convert PDF\Image To Word

August 21, 2025 Posted by Tôi Hùng ✅ , No comments
Chào mọi người.
Hôm qua chị mình hỏi có cách nào để chuyển File PDF sáng Word nhanh không?
Mình đã tìm hiểu nhưng có rất ít Website chuyển đổi, thế là mình bắt tay vào tìm hiểu việc chuyển đổi để giúp việc chuyển đổi đó tiện hơn.
Video Demo.

Full Source Code.

import sys
import os
import re
import tempfile
import subprocess
import shutil
from docx import Document
from docx.shared import Inches
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QTextEdit, QFileDialog,
                             QLabel, QHBoxLayout, QLineEdit, QMessageBox, QProgressBar,
                             QScrollArea, QDialog, QGridLayout, QTabWidget)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QPixmap, QClipboard
from google import genai
from PIL import Image
import io

class ImagePreviewDialog(QDialog):
    def __init__(self, images, parent=None):
        super().__init__(parent)
        self.images = images
        self.initUI()

    def initUI(self):
        self.setWindowTitle(f'Xem trước {len(self.images)} hình ảnh')
        self.setGeometry(200, 200, 800, 600)

        layout = QVBoxLayout()

        # Create scroll area
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)

        # Create widget to hold images
        content_widget = QWidget()
        grid_layout = QGridLayout(content_widget)

        # Add images to grid (2 columns)
        row = 0
        col = 0
        for i, img_info in enumerate(self.images):
            try:
                # Create label for image
                img_label = QLabel()
                pixmap = QPixmap(img_info['path'])

                # Scale image to fit
                scaled_pixmap = pixmap.scaled(300, 300, Qt.AspectRatioMode.KeepAspectRatio,
                                            Qt.TransformationMode.SmoothTransformation)
                img_label.setPixmap(scaled_pixmap)
                img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

                # Create info label
                info_text = f"HÌNH ẢNH {i+1}\nKích thước: {img_info.get('size_info', 'N/A')}\nNguồn: {img_info.get('source', 'N/A')}"
                info_label = QLabel(info_text)
                info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
                info_label.setStyleSheet("font-weight: bold; margin: 5px; padding: 5px; border: 1px solid #ccc; border-radius: 3px;")

                # Add to grid
                img_container = QVBoxLayout()
                img_container.addWidget(info_label)
                img_container.addWidget(img_label)

                container_widget = QWidget()
                container_widget.setLayout(img_container)

                grid_layout.addWidget(container_widget, row, col)

                # Move to next position
                col += 1
                if col >= 2:  # 2 columns
                    col = 0
                    row += 1

            except Exception as e:
                print(f"Error loading image {i+1}: {e}")

        scroll.setWidget(content_widget)
        layout.addWidget(scroll)

        # Close button
        close_button = QPushButton('Đóng')
        close_button.clicked.connect(self.accept)
        layout.addWidget(close_button)

        self.setLayout(layout)

class ConversionThread(QThread):
    progress = pyqtSignal(int)
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, client, uploaded_file, prompt):
        super().__init__()
        self.client = client
        self.uploaded_file = uploaded_file
        self.prompt = prompt
        self.max_retries = 3
        self.retry_delay = 60
        self.is_running = True

    def run(self):
        for attempt in range(self.max_retries):
            if not self.is_running:
                return
            try:
                # Simulate progress
                for i in range(0, 51):
                    if not self.is_running:
                        return
                    self.progress.emit(i)
                    self.msleep(50)

                # Progress simulation during generation
                for i in range(51, 101):
                    if not self.is_running:
                        return
                    self.progress.emit(i)
                    self.msleep(30)

                # Generate content using new simplified API
                response = self.client.models.generate_content(
                    model="gemini-2.5-flash",
                    contents=[self.uploaded_file, self.prompt],
                )

                self.finished.emit(response.text)
                return

            except Exception as e:
                if "429" in str(e) and attempt < self.max_retries - 1:
                    self.error.emit(f"Rate limit exceeded. Retrying in {self.retry_delay} seconds...")
                    self.msleep(self.retry_delay * 1000)
                else:
                    self.error.emit(str(e))
                    return

    def stop(self):
        self.is_running = False

class WordTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent_converter = parent
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()

        # Title
        title_label = QLabel("PDF to Word Converter")
        title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
        layout.addWidget(title_label)

        # Convert button
        self.convert_button = QPushButton('Convert PDF to Text')
        self.convert_button.clicked.connect(self.convert_pdf_to_text)
        self.convert_button.setEnabled(False)
        layout.addWidget(self.convert_button)

        # Export buttons
        export_layout = QHBoxLayout()
        self.export_word_button = QPushButton('Export to Word Document (python-docx)')
        self.export_word_button.clicked.connect(self.export_to_word)
        self.export_word_button.setEnabled(False)

        self.export_pandoc_button = QPushButton('Export to Word with Pandoc (Math formulas)')
        self.export_pandoc_button.clicked.connect(self.export_to_word_pandoc)
        self.export_pandoc_button.setEnabled(False)

        export_layout.addWidget(self.export_word_button)
        export_layout.addWidget(self.export_pandoc_button)
        layout.addLayout(export_layout)

        # Result display
        self.result_text = QTextEdit()
        self.result_text.setReadOnly(True)
        layout.addWidget(QLabel('Results:'))
        layout.addWidget(self.result_text)

        self.setLayout(layout)

    def convert_pdf_to_text(self):
        if not self.parent_converter.client:
            QMessageBox.warning(self, "Error", "Please set the API Key first.")
            return

        if not self.parent_converter.uploaded_file:
            self.result_text.setText("Please upload a PDF file first.")
            return

        prompt = """
        Hãy nhận diện và gõ lại [CHÍNH XÁC] toàn bộ nội dung PDF thành văn bản, bao gồm tất cả công thức Toán học được bọc trong dấu $.

        [YÊU CẦU NGHIÊM NGẶT]:
        - CHỈ gõ lại nội dung có trong PDF
        - KHÔNG thêm bất kỳ nội dung nào khác
        - Giữ nguyên cấu trúc và định dạng của văn bản gốc, bỏ qua phần hình vẽ trong PDF
        - [Bắt buộc] tất cả công thức toán học viết dưới dạng LaTeX được bọc trong dấu $
        """

        self.parent_converter.start_conversion(prompt, result_widget=self.result_text,
                                             convert_button=self.convert_button,
                                             export_buttons=[self.export_word_button, self.export_pandoc_button])

    def export_to_word(self):
        if not hasattr(self.parent_converter, 'pdf_text') or not self.parent_converter.pdf_text:
            QMessageBox.warning(self, "Error", "Please convert PDF to text first.")
            return

        # Save dialog
        file_dialog = QFileDialog()
        output_path, _ = file_dialog.getSaveFileName(self, "Save Word Document", "", "Word Documents (*.docx)")

        if output_path:
            try:
                self.parent_converter.export_with_python_docx(output_path)
            except Exception as e:
                QMessageBox.warning(self, "Error", f"An error occurred during export:\n{str(e)}")

    def export_to_word_pandoc(self):
        if not hasattr(self.parent_converter, 'pdf_text') or not self.parent_converter.pdf_text:
            QMessageBox.warning(self, "Error", "Please convert PDF to text first.")
            return

        # Save dialog
        file_dialog = QFileDialog()
        output_path, _ = file_dialog.getSaveFileName(self, "Save Word Document (Pandoc)", "", "Word Documents (*.docx)")

        if output_path:
            try:
                self.export_with_pandoc(output_path)
            except Exception as e:
                QMessageBox.warning(self, "Error", f"An error occurred during pandoc export:\n{str(e)}")

    def export_with_pandoc(self, output_path):
        """Export using pandoc for better math formula handling"""
        try:
            # Create temporary markdown file
            temp_md = tempfile.mktemp(suffix='.md')

            # Write to temporary markdown file
            with open(temp_md, 'w', encoding='utf-8') as f:
                f.write(self.parent_converter.pdf_text)

            # Run pandoc command
            cmd = [
                'pandoc',
                temp_md,
                '-o', output_path,
                '--from=markdown',
                '--to=docx',
                '--standalone'
            ]

            result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')

            if result.returncode == 0:
                QMessageBox.information(self, "Export Complete",
                                      f"Document exported successfully with Pandoc to:\n{output_path}\n\n"
                                      f"Math formulas should be properly rendered.")
            else:
                QMessageBox.warning(self, "Pandoc Error",
                                  f"Pandoc failed with error:\n{result.stderr}")

            # Clean up
            if os.path.exists(temp_md):
                os.unlink(temp_md)

        except FileNotFoundError:
            QMessageBox.warning(self, "Pandoc Not Found",
                              "Pandoc is not installed or not found in PATH.\n"
                              "Please install Pandoc from https://pandoc.org/")
        except Exception as e:
            QMessageBox.warning(self, "Error", f"An error occurred during pandoc export:\n{str(e)}")

class ImageTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent_converter = parent
        self.uploaded_images = []
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()

        # Title
        title_label = QLabel("Image to Word Converter")
        title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
        layout.addWidget(title_label)

        # Upload buttons
        upload_layout = QHBoxLayout()
        self.upload_images_button = QPushButton('Upload Images')
        self.upload_images_button.clicked.connect(self.upload_images)

        self.paste_from_clipboard_button = QPushButton('Paste from Clipboard')
        self.paste_from_clipboard_button.clicked.connect(self.paste_from_clipboard)

        self.clear_images_button = QPushButton('Clear All Images')
        self.clear_images_button.clicked.connect(self.clear_images)

        upload_layout.addWidget(self.upload_images_button)
        upload_layout.addWidget(self.paste_from_clipboard_button)
        upload_layout.addWidget(self.clear_images_button)
        layout.addLayout(upload_layout)

        # Image status
        self.image_status_label = QLabel("No images loaded")
        layout.addWidget(self.image_status_label)

        # Preview button
        self.preview_images_button = QPushButton('Preview Images')
        self.preview_images_button.clicked.connect(self.show_image_preview)
        self.preview_images_button.setEnabled(False)
        layout.addWidget(self.preview_images_button)

        # Convert button
        self.convert_button = QPushButton('Convert Images to Text')
        self.convert_button.clicked.connect(self.convert_images_to_text)
        self.convert_button.setEnabled(False)
        layout.addWidget(self.convert_button)

        # Export buttons
        export_layout = QHBoxLayout()
        self.export_word_button = QPushButton('Export to Word Document (python-docx)')
        self.export_word_button.clicked.connect(self.export_to_word)
        self.export_word_button.setEnabled(False)

        self.export_pandoc_button = QPushButton('Export to Word with Pandoc (Math formulas)')
        self.export_pandoc_button.clicked.connect(self.export_to_word_pandoc)
        self.export_pandoc_button.setEnabled(False)

        export_layout.addWidget(self.export_word_button)
        export_layout.addWidget(self.export_pandoc_button)
        layout.addLayout(export_layout)

        # Result display
        self.result_text = QTextEdit()
        self.result_text.setReadOnly(True)
        layout.addWidget(QLabel('Results:'))
        layout.addWidget(self.result_text)

        self.setLayout(layout)

    def upload_images(self):
        file_dialog = QFileDialog()
        file_paths, _ = file_dialog.getOpenFileNames(
            self,
            "Select Image files",
            "",
            "Image Files (*.png *.jpg *.jpeg *.bmp *.gif *.tiff)"
        )

        if file_paths:
            self.add_images_from_files(file_paths)

    def paste_from_clipboard(self):
        clipboard = QApplication.clipboard()
        mime_data = clipboard.mimeData()

        if mime_data.hasImage():
            image = clipboard.image()
            if not image.isNull():
                # Save clipboard image to temp file
                temp_path = tempfile.mktemp(suffix='.png')
                if image.save(temp_path, 'PNG'):
                    self.add_images_from_files([temp_path], source='Clipboard')
                    QMessageBox.information(self, "Success", "Image pasted from clipboard!")
                else:
                    QMessageBox.warning(self, "Error", "Failed to save clipboard image.")
            else:
                QMessageBox.warning(self, "Error", "No valid image in clipboard.")
        else:
            QMessageBox.warning(self, "Error", "No image found in clipboard.")

    def add_images_from_files(self, file_paths, source='Upload'):
        for file_path in file_paths:
            try:
                # Create a copy in the output directory if needed
                if self.parent_converter.output_dir:
                    img_dir = os.path.join(self.parent_converter.output_dir, "Images")
                    if not os.path.exists(img_dir):
                        os.makedirs(img_dir)

                    filename = f"img_{len(self.uploaded_images) + 1}_{os.path.basename(file_path)}"
                    dest_path = os.path.join(img_dir, filename)
                    shutil.copy2(file_path, dest_path)

                    # Get image size info
                    try:
                        with Image.open(dest_path) as pil_img:
                            width, height = pil_img.size
                            size_info = f"{width}x{height}px"
                    except:
                        size_info = "Unknown"

                    self.uploaded_images.append({
                        'path': dest_path,
                        'filename': filename,
                        'source': source,
                        'size_info': size_info,
                        'index': len(self.uploaded_images) + 1
                    })
                else:
                    # If no output directory, use original path
                    try:
                        with Image.open(file_path) as pil_img:
                            width, height = pil_img.size
                            size_info = f"{width}x{height}px"
                    except:
                        size_info = "Unknown"

                    self.uploaded_images.append({
                        'path': file_path,
                        'filename': os.path.basename(file_path),
                        'source': source,
                        'size_info': size_info,
                        'index': len(self.uploaded_images) + 1
                    })

            except Exception as e:
                QMessageBox.warning(self, "Error", f"Failed to process {file_path}:\n{str(e)}")

        self.update_image_status()

    def clear_images(self):
        self.uploaded_images = []
        self.update_image_status()

    def update_image_status(self):
        if self.uploaded_images:
            self.image_status_label.setText(f"Loaded {len(self.uploaded_images)} images")
            self.preview_images_button.setEnabled(True)
            self.convert_button.setEnabled(True)
        else:
            self.image_status_label.setText("No images loaded")
            self.preview_images_button.setEnabled(False)
            self.convert_button.setEnabled(False)
            self.export_word_button.setEnabled(False)
            self.export_pandoc_button.setEnabled(False)

    def show_image_preview(self):
        if not self.uploaded_images:
            QMessageBox.information(self, "Info", "No images to preview.")
            return

        dialog = ImagePreviewDialog(self.uploaded_images, self)
        dialog.exec()

    def convert_images_to_text(self):
        if not self.parent_converter.client:
            QMessageBox.warning(self, "Error", "Please set the API Key first.")
            return

        if not self.uploaded_images:
            self.result_text.setText("Please upload images first.")
            return

        # Upload all images to Gemini
        try:
            uploaded_files = []
            for img_info in self.uploaded_images:
                uploaded_file = self.parent_converter.client.files.upload(file=img_info['path'])
                uploaded_files.append(uploaded_file)

            prompt = f"""
            Hãy nhận diện và gõ lại [CHÍNH XÁC] toàn bộ nội dung trong {len(self.uploaded_images)} hình ảnh thành văn bản, bao gồm tất cả công thức Toán học được bọc trong dấu $.

            [QUY TẮC NGHIÊM NGẶT]:
            - CHỈ gõ lại nội dung có trong hình ảnh
            - KHÔNG thêm bất kỳ nội dung nào khác
            - Giữ nguyên cấu trúc và định dạng của văn bản gốc, bỏ qua phần hình vẽ trong PDF
            - [Bắt buộc] tất cả công thức toán học viết dưới dạng LaTeX được bọc trong dấu $

            """

            # Create content list with all uploaded files
            content_list = uploaded_files + [prompt]

            # Start conversion with multiple images
            self.start_image_conversion(content_list)

        except Exception as e:
            QMessageBox.warning(self, "Error", f"Failed to upload images: {str(e)}")

    def start_image_conversion(self, content_list):
        self.convert_button.setEnabled(False)
        self.export_word_button.setEnabled(False)
        self.export_pandoc_button.setEnabled(False)

        self.parent_converter.progress_bar.setValue(0)
        self.result_text.clear()
        self.result_text.append("Starting image conversion process...")
        self.parent_converter.status_label.setText("Status: Converting images...")

        # Create a special thread for image conversion
        self.image_conversion_thread = ImageConversionThread(
            self.parent_converter.client,
            content_list
        )
        self.image_conversion_thread.progress.connect(self.parent_converter.update_progress)
        self.image_conversion_thread.finished.connect(self.on_conversion_finished)
        self.image_conversion_thread.error.connect(self.on_conversion_error)
        self.image_conversion_thread.start()

    def on_conversion_finished(self, text):
        # Save processed text
        self.parent_converter.pdf_text = self.parent_converter.process_formulas(text)

        self.result_text.clear()
        self.result_text.append("Images converted successfully. Here's the content:\n\n")
        self.result_text.append(self.parent_converter.pdf_text)

        self.convert_button.setEnabled(True)
        self.export_word_button.setEnabled(True)
        self.export_pandoc_button.setEnabled(True)

        self.parent_converter.status_label.setText("Status: Image conversion completed")

    def on_conversion_error(self, error_message):
        self.result_text.append(f"An error occurred during conversion: {error_message}")
        self.convert_button.setEnabled(True)
        self.parent_converter.status_label.setText("Status: Error occurred")

    def export_to_word(self):
        if not hasattr(self.parent_converter, 'pdf_text') or not self.parent_converter.pdf_text:
            QMessageBox.warning(self, "Error", "Please convert images to text first.")
            return

        # Save dialog
        file_dialog = QFileDialog()
        output_path, _ = file_dialog.getSaveFileName(self, "Save Word Document", "", "Word Documents (*.docx)")

        if output_path:
            try:
                self.parent_converter.export_with_python_docx(output_path, include_original_images=True)
            except Exception as e:
                QMessageBox.warning(self, "Error", f"An error occurred during export:\n{str(e)}")

    def export_to_word_pandoc(self):
        if not hasattr(self.parent_converter, 'pdf_text') or not self.parent_converter.pdf_text:
            QMessageBox.warning(self, "Error", "Please convert images to text first.")
            return

        # Save dialog
        file_dialog = QFileDialog()
        output_path, _ = file_dialog.getSaveFileName(self, "Save Word Document (Pandoc)", "", "Word Documents (*.docx)")

        if output_path:
            try:
                self.export_with_pandoc(output_path)
            except Exception as e:
                QMessageBox.warning(self, "Error", f"An error occurred during pandoc export:\n{str(e)}")

    def export_with_pandoc(self, output_path):
        """Export using pandoc for better math formula handling"""
        try:
            # Create temporary markdown file
            temp_md = tempfile.mktemp(suffix='.md')

            # Write to temporary markdown file
            with open(temp_md, 'w', encoding='utf-8') as f:
                f.write(self.parent_converter.pdf_text)

            # Run pandoc command
            cmd = [
                'pandoc',
                temp_md,
                '-o', output_path,
                '--from=markdown',
                '--to=docx',
                '--standalone'
            ]

            result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')

            if result.returncode == 0:
                QMessageBox.information(self, "Export Complete",
                                      f"Document exported successfully with Pandoc to:\n{output_path}\n\n"
                                      f"Math formulas should be properly rendered.")
            else:
                QMessageBox.warning(self, "Pandoc Error",
                                  f"Pandoc failed with error:\n{result.stderr}")

            # Clean up
            if os.path.exists(temp_md):
                os.unlink(temp_md)

        except FileNotFoundError:
            QMessageBox.warning(self, "Pandoc Not Found",
                              "Pandoc is not installed or not found in PATH.\n"
                              "Please install Pandoc from https://pandoc.org/")
        except Exception as e:
            QMessageBox.warning(self, "Error", f"An error occurred during pandoc export:\n{str(e)}")

class ImageConversionThread(QThread):
    progress = pyqtSignal(int)
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, client, content_list):
        super().__init__()
        self.client = client
        self.content_list = content_list
        self.max_retries = 3
        self.retry_delay = 60
        self.is_running = True

    def run(self):
        for attempt in range(self.max_retries):
            if not self.is_running:
                return
            try:
                # Simulate progress
                for i in range(0, 51):
                    if not self.is_running:
                        return
                    self.progress.emit(i)
                    self.msleep(50)

                # Progress simulation during generation
                for i in range(51, 101):
                    if not self.is_running:
                        return
                    self.progress.emit(i)
                    self.msleep(30)

                # Generate content using new simplified API
                response = self.client.models.generate_content(
                    model="gemini-2.5-flash",
                    contents=self.content_list,
                )

                self.finished.emit(response.text)
                return

            except Exception as e:
                if "429" in str(e) and attempt < self.max_retries - 1:
                    self.error.emit(f"Rate limit exceeded. Retrying in {self.retry_delay} seconds...")
                    self.msleep(self.retry_delay * 1000)
                else:
                    self.error.emit(str(e))
                    return

    def stop(self):
        self.is_running = False

class PDFToTextConverter(QWidget):
    def __init__(self):
        super().__init__()
        self.api_key = ""
        self.file_path = None
        self.uploaded_file = None
        self.client = None
        self.pdf_text = ""
        self.output_dir = ""
        self.initUI()
        self.load_api_key()
        self.conversion_thread = None

    def initUI(self):
        main_layout = QVBoxLayout()

        # API Key section
        api_layout = QHBoxLayout()
        self.api_key_input = QLineEdit()
        self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
        self.api_key_button = QPushButton('Set API Key')
        self.api_key_button.clicked.connect(self.set_api_key)
        self.edit_api_key_button = QPushButton('Edit API Key')
        self.edit_api_key_button.clicked.connect(self.edit_api_key)
        self.edit_api_key_button.setEnabled(False)
        api_layout.addWidget(QLabel('Gemini API Key:'))
        api_layout.addWidget(self.api_key_input)
        api_layout.addWidget(self.api_key_button)
        api_layout.addWidget(self.edit_api_key_button)
        main_layout.addLayout(api_layout)

        # File upload section
        upload_layout = QHBoxLayout()
        self.upload_pdf_button = QPushButton('Upload PDF')
        self.upload_pdf_button.clicked.connect(self.upload_pdf)
        self.file_label = QLabel('No PDF file selected')
        upload_layout.addWidget(self.upload_pdf_button)
        upload_layout.addWidget(self.file_label)
        main_layout.addLayout(upload_layout)

        # Progress bar
        self.progress_bar = QProgressBar(self)
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        main_layout.addWidget(QLabel("Progress:"))
        main_layout.addWidget(self.progress_bar)

        # Status label
        self.status_label = QLabel("Status: Idle")
        main_layout.addWidget(self.status_label)

        # Create tab widget
        self.tab_widget = QTabWidget()

        # Create tabs
        self.word_tab = WordTab(self)
        self.image_tab = ImageTab(self)

        # Add tabs
        self.tab_widget.addTab(self.word_tab, "PDF to Word")
        self.tab_widget.addTab(self.image_tab, "Image to Word")

        main_layout.addWidget(self.tab_widget)

        self.setLayout(main_layout)
        self.setWindowTitle('PDF & Image to Word Converter')
        self.setGeometry(300, 300, 1200, 900)

    def set_api_key(self):
        self.api_key = self.api_key_input.text()
        if self.api_key:
            self.setup_client()
            self.api_key_input.setEnabled(False)
            self.api_key_button.setEnabled(False)
            self.edit_api_key_button.setEnabled(True)
            self.save_api_key()
            QMessageBox.information(self, "Success", "API Key set successfully!")
        else:
            QMessageBox.warning(self, "Error", "Please enter an API Key.")

    def setup_client(self):
        try:
            self.client = genai.Client(api_key=self.api_key)
            print("Gemini client initialized successfully")
        except Exception as e:
            print(f"Error setting up client: {e}")
            self.client = None

    def edit_api_key(self):
        self.api_key_input.setEnabled(True)
        self.api_key_button.setEnabled(True)
        self.edit_api_key_button.setEnabled(False)

    def save_api_key(self):
        try:
            with open('api_key.txt', 'w') as f:
                f.write(self.api_key)
        except Exception as e:
            print(f"Error saving API key: {str(e)}")

    def load_api_key(self):
        try:
            if os.path.exists('api_key.txt'):
                with open('api_key.txt', 'r') as f:
                    self.api_key = f.read().strip()
                    self.api_key_input.setText(self.api_key)
                    self.set_api_key()
        except Exception as e:
            print(f"Error loading API key: {str(e)}")

    def upload_pdf(self):
        if not self.client:
            QMessageBox.warning(self, "Error", "Please set the API Key first.")
            return

        file_dialog = QFileDialog()
        self.file_path, _ = file_dialog.getOpenFileName(self, "Select PDF file", "", "PDF Files (*.pdf)")

        if self.file_path:
            file_name = os.path.basename(self.file_path)
            self.file_label.setText(f"File: {file_name}")

            # Set output directory to the same directory as the PDF file
            self.output_dir = os.path.dirname(self.file_path)

            self.process_pdf()

    def process_pdf(self):
        self.status_label.setText("Status: Processing PDF...")

        try:
            # Upload PDF using new API
            safe_path = self.file_path.replace("\\", "/")

            # Upload file using new API
            self.uploaded_file = self.client.files.upload(file=safe_path)

            print(f"File uploaded successfully: {self.uploaded_file.uri}")

            # Enable convert button in PDF tab
            self.word_tab.convert_button.setEnabled(True)

            self.status_label.setText("Status: PDF ready for conversion")

        except Exception as e:
            print(f"Error processing PDF: {e}")
            QMessageBox.warning(self, "Error", f"Failed to upload PDF: {str(e)}")

    def start_conversion(self, prompt, result_widget=None, convert_button=None, export_buttons=None):
        if convert_button:
            convert_button.setEnabled(False)
        if export_buttons:
            for btn in export_buttons:
                btn.setEnabled(False)

        self.progress_bar.setValue(0)
        if result_widget:
            result_widget.clear()
            result_widget.append("Starting conversion process...")
        self.status_label.setText("Status: Converting...")

        # Start conversion thread
        self.conversion_thread = ConversionThread(self.client, self.uploaded_file, prompt)
        self.conversion_thread.progress.connect(self.update_progress)
        self.conversion_thread.finished.connect(
            lambda text: self.on_conversion_finished(text, result_widget, convert_button, export_buttons)
        )
        self.conversion_thread.error.connect(
            lambda error: self.on_conversion_error(error, result_widget, convert_button)
        )
        self.conversion_thread.start()

    def update_progress(self, value):
        self.progress_bar.setValue(value)

    def on_conversion_finished(self, text, result_widget=None, convert_button=None, export_buttons=None):
        # Save processed text
        self.pdf_text = self.process_formulas(text)

        if result_widget:
            result_widget.clear()
            result_widget.append("PDF converted successfully. Here's the content:\n\n")
            result_widget.append(self.pdf_text)

        if convert_button:
            convert_button.setEnabled(True)
        if export_buttons:
            for btn in export_buttons:
                btn.setEnabled(True)

        self.status_label.setText("Status: Conversion completed")

    def on_conversion_error(self, error_message, result_widget=None, convert_button=None):
        if result_widget:
            result_widget.append(f"An error occurred during conversion: {error_message}")
        if convert_button:
            convert_button.setEnabled(True)
        self.status_label.setText("Status: Error occurred")

    def process_formulas(self, text):
        def process_math_content(match):
            content = match.group(1)
            content = content.replace('π', '\\pi')
            content = re.sub(r'√(\d+)', r'\\sqrt{\1}', content)
            content = re.sub(r'√\{([^}]+)\}', r'\\sqrt{\1}', content)
            content = content.replace('≠', '\\neq')
            content = content.replace('*', '')
            return f'${content}$'

        text = re.sub(r'\$(.+?)\$', process_math_content, text, flags=re.DOTALL)
        return text

    def export_with_python_docx(self, output_path, include_original_images=False):
        """Export using python-docx with optional original images"""
        doc = Document()
        doc.add_heading('Converted Document', 0)

        lines = self.pdf_text.split('\n')
        i = 0

        # If this is from image tab and we want to include original images
        if include_original_images and hasattr(self.image_tab, 'uploaded_images'):
            doc.add_heading('Original Images', level=1)
            for img_info in self.image_tab.uploaded_images:
                try:
                    if os.path.exists(img_info['path']):
                        doc.add_paragraph(f"Image {img_info['index']}: {img_info['filename']}")
                        doc.add_picture(img_info['path'], width=Inches(5))
                        doc.add_paragraph("")  # spacing
                except Exception as e:
                    print(f"Error adding original image {img_info['index']}: {e}")
                    doc.add_paragraph(f"[Error: Could not insert image {img_info['index']}]")

            doc.add_heading('Extracted Text', level=1)

        while i < len(lines):
            line = lines[i].strip()
            if not line:
                i += 1
                continue

            # Check for Markdown table
            if '|' in line and len(line.split('|')) > 2:
                table_lines = []
                j = i

                # Collect consecutive table lines
                while j < len(lines):
                    current_line = lines[j].strip()
                    if '|' in current_line and len(current_line.split('|')) > 2:
                        # Skip separator lines like |---|---|
                        separator_pattern = r'^\s*\|[\s\-\:]*\|\s*$'
                        if not re.match(separator_pattern, current_line):
                            table_lines.append(current_line)
                        j += 1
                    else:
                        break

                if table_lines:
                    print(f"Creating table with {len(table_lines)} rows")
                    self.create_word_table(doc, table_lines)
                    i = j
                else:
                    doc.add_paragraph(line)
                    i += 1
            else:
                # Regular text
                doc.add_paragraph(line)
                i += 1

        doc.save(output_path)

        success_msg = (f"Document exported successfully to:\n{output_path}\n\n"
                      f"Statistics:\n"
                      f"- Processed {len(lines)} lines of text")

        if include_original_images and hasattr(self.image_tab, 'uploaded_images'):
            success_msg += f"\n- Included {len(self.image_tab.uploaded_images)} original images"

        print(success_msg)
        QMessageBox.information(self, "Export Complete", success_msg)

    def create_word_table(self, doc, table_lines):
        """Create a Word table from Markdown table lines"""
        if not table_lines:
            return

        print(f"Creating table from {len(table_lines)} lines:")
        for i, line in enumerate(table_lines):
            print(f"  Line {i}: {repr(line)}")

        # Parse table data
        table_data = []
        for line_num, line in enumerate(table_lines):
            if not line.strip():
                continue

            # Split by | and clean up cells
            raw_cells = line.split('|')
            cells = []

            for cell in raw_cells:
                cleaned_cell = cell.strip()
                if cleaned_cell or (cells and line_num < len(table_lines) - 1):
                    cells.append(cleaned_cell)

            # Remove leading/trailing empty cells only
            while cells and not cells[0]:
                cells.pop(0)
            while cells and not cells[-1]:
                cells.pop()

            if cells:
                table_data.append(cells)
                print(f"  Parsed row {len(table_data)}: {cells}")

        if not table_data:
            print("No valid table data found, adding as regular text")
            for line in table_lines:
                doc.add_paragraph(line)
            return

        try:
            rows = len(table_data)
            cols = max(len(row) for row in table_data) if table_data else 0

            print(f"Creating {rows}x{cols} table")

            if rows > 0 and cols > 0:
                # Create table
                table = doc.add_table(rows=rows, cols=cols)
                table.style = 'Table Grid'

                # Fill table with data
                for i, row_data in enumerate(table_data):
                    if i >= len(table.rows):
                        print(f"Warning: Row {i} exceeds table rows")
                        break

                    row = table.rows[i]
                    for j, cell_data in enumerate(row_data):
                        if j >= len(row.cells):
                            print(f"Warning: Column {j} exceeds table columns in row {i}")
                            break

                        try:
                            clean_data = str(cell_data).replace('\n', ' ').replace('\r', '')
                            row.cells[j].text = clean_data
                            print(f"  Cell [{i}][{j}]: {repr(clean_data)}")
                        except Exception as cell_error:
                            print(f"Error setting cell [{i}][{j}]: {cell_error}")
                            row.cells[j].text = str(cell_data)

                doc.add_paragraph("")  # spacing after table
                print("Table created successfully")

            else:
                print(f"Invalid table dimensions: {rows}x{cols}")
                for line in table_lines:
                    doc.add_paragraph(line)

        except Exception as e:
            print(f"Error creating table: {e}")
            print(f"Table data that caused error: {table_data}")
            doc.add_paragraph("Table conversion failed, showing as text:")
            for line in table_lines:
                doc.add_paragraph(f"  {line}")

    def closeEvent(self, event):
        self.cleanup_and_close()
        super().closeEvent(event)

    def cleanup_and_close(self):
        # Stop conversion thread if running
        if self.conversion_thread and self.conversion_thread.isRunning():
            self.conversion_thread.stop()
            self.conversion_thread.wait()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = PDFToTextConverter()
    ex.show()
    sys.exit(app.exec())

Hiện demo mình đang tích hợp thêm nhiều sản phẩm khác nên chưa phát hành, mình sẽ cập nhật DEMO TEST cho mọi người sớm nhất.

0 Comments:

Post a Comment

Top