import io
import os
import sys
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
# Стабильные библиотеки для препресса
from pypdf import PdfReader, PdfWriter
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
def get_resource_path(relative_path):
"""Определяет правильный путь к ресурсам при упаковке в .exe."""
if hasattr(sys, "_MEIPASS"):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
def create_stamp_pdf(img_path, x_mm, y_mm, w_mm, h_mm, page_width, page_height):
"""Создает в памяти PDF-страницу со штампом в режиме Multiply."""
packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=(page_width, page_height))
can.saveState()
can.setBlendMode("Multiply")
# Пересчет координат (в ReportLab отсчет идет снизу вверх)
y_points = page_height - (y_mm * mm) - (h_mm * mm)
x_points = x_mm * mm
can.drawImage(
img_path,
x_points,
y_points,
width=w_mm * mm,
height=h_mm * mm,
mask="auto",
)
can.restoreState()
can.save()
packet.seek(0)
# ИСПРАВЛЕНО: Явно берем первую страницу [0] из созданного штампа
return PdfReader(packet).pages[0]
def process_pdf_worker(
pdf_path,
img_path,
x_mm,
y_mm,
w_mm,
h_mm,
progress_window,
progress_bar,
status_label,
):
"""Функция обработки, выполняемая в фоновом потоке."""
try:
reader = PdfReader(pdf_path)
writer = PdfWriter()
total_pages = len(reader.pages)
# Конфигурируем прогресс-бар в отдельном окне
progress_bar.config(maximum=total_pages)
progress_window.update_idletasks()
# ИСПРАВЛЕНО: Корректно берем размеры первой страницы через индекс [0]
first_page = reader.pages[0]
page_width = float(first_page.mediabox.width)
page_height = float(first_page.mediabox.height)
# Создаем прозрачный оверлей (теперь возвращает чистый объект страницы)
stamp_page = create_stamp_pdf(
img_path, x_mm, y_mm, w_mm, h_mm, page_width, page_height
)
# Пакетное наложение слоев
for i in range(total_pages):
page = reader.pages[i]
# Метод merge_page просто соединяет два слоя. Текст оригинала не меняется.
page.merge_page(stamp_page)
writer.add_page(page)
# Обновляем прогресс-бар и текст в отдельном окне
progress_bar.config(value=i + 1)
status_label.config(
text=f"Обработано страниц: {i + 1} из {total_pages}"
)
progress_window.update_idletasks()
# Формируем имя для сохранения
dir_name, file_name = os.path.split(pdf_path)
output_path = os.path.join(dir_name, "ready_" + file_name)
with open(output_path, "wb") as f:
writer.write(f)
# Закрываем окно прогресса перед выводом финального сообщения
progress_window.destroy()
# Успешное завершение: показываем окно и закрываем всю программу
messagebox.showinfo(
"Успех", f"Файл успешно сохранен как:\n{output_path}"
)
root.quit() # Полное закрытие программы
except Exception as e:
if progress_window.winfo_exists():
progress_window.destroy() # Закрываем окно прогресса при ошибке
messagebox.showerror(
"Ошибка обработки", f"Произошла ошибка при изменении PDF:\n{str(e)}"
)
root.deiconify() # Возвращаем главное окно, если что-то пошло не так
def start_processing():
"""Собирает данные, открывает окно прогресса и запускает поток."""
# 1. Выбор исходного PDF-файла
pdf_path = filedialog.askopenfilename(
title="Выберите исходный PDF-документ",
filetypes=[("PDF файлы", "*.pdf")],
)
if not pdf_path:
return
# 2. Выбор изображения
img_path = filedialog.askopenfilename(
title="Выберите изображение для вставки",
filetypes=[
("Изображения", "*.png *.jpg *.jpeg *.bmp"),
("Все файлы", "*.*"),
],
)
if not img_path:
return
# 3. Считывание координат
try:
x_mm = float(entry_x.get())
y_mm = float(entry_y.get())
w_mm = float(entry_w.get())
h_mm = float(entry_h.get())
except ValueError:
messagebox.showerror(
"Ошибка", "Пожалуйста, введите корректные числа в поля координат."
)
return
# Скрываем главное окно настроек на время работы
root.withdraw()
# --- Создание ОТДЕЛЬНОГО окна для прогресс-бара ---
progress_window = tk.Toplevel(root)
progress_window.title("Обработка...")
progress_window.geometry("320x100")
progress_window.resizable(False, False)
# Блокируем взаимодействие с другими окнами, пока это открыто
progress_window.grab_set()
# Чтобы пользователь случайно не закрыл окно прогресса крестиком и не сломал поток
progress_window.protocol("WM_DELETE_WINDOW", lambda: None)
status_label = tk.Label(
progress_window, text="Подготовка к обработке...", font=("Arial", 10)
)
status_label.pack(pady=10)
progress_bar = ttk.Progressbar(
progress_window, orient="horizontal", mode="determinate", length=280
)
progress_bar.pack(padx=20, pady=5)
# Запуск обработки в отдельном фоновом потоке
threading.Thread(
target=process_pdf_worker,
args=(
pdf_path,
img_path,
x_mm,
y_mm,
w_mm,
h_mm,
progress_window,
progress_bar,
status_label,
),
daemon=True,
).start()
# --- Создание графического интерфейса ---
root = tk.Tk()
root.title("Вставка изображения в PDF (Multiply)")
root.geometry("380x280")
root.resizable(False, False)
frame = tk.LabelFrame(root, text=" Настройки положения (в мм) ")
frame.pack(padx=15, pady=15, fill="x", ipady=5)
# Поля ввода
tk.Label(frame, text="Отступ слева (X):").grid(
row=0, column=0, sticky="w", pady=5, padx=10
)
entry_x = tk.Entry(frame, width=10)
entry_x.insert(0, "20")
entry_x.grid(row=0, column=1, pady=5)
tk.Label(frame, text="Отступ сверху (Y):").grid(
row=1, column=0, sticky="w", pady=5, padx=10
)
entry_y = tk.Entry(frame, width=10)
entry_y.insert(0, "20")
entry_y.grid(row=1, column=1, pady=5)
tk.Label(frame, text="Ширина картинки:").grid(
row=2, column=0, sticky="w", pady=5, padx=10
)
entry_w = tk.Entry(frame, width=10)
entry_w.insert(0, "50")
entry_w.grid(row=2, column=1, pady=5)
tk.Label(frame, text="Высота картинки:").grid(
row=3, column=0, sticky="w", pady=5, padx=10
)
entry_h = tk.Entry(frame, width=10)
entry_h.insert(0, "50")
entry_h.grid(row=3, column=1, pady=5)
# Кнопка запуска процесса
btn_start = tk.Button(
root,
text="Выбрать файлы и запустить",
command=start_processing,
font=("Segoe UI", 10),
)
btn_start.pack(fill="x", padx=15, pady=5, ipady=5)
root.mainloop()