[PS CC2025] Печать на 200 стр

Ну, можно и чуть менее универсально
Python:
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()

Ну, и теперь есть exe.
Не совсем корректно работает на "сложных" файлах (скомпанованных из разных документов в один) - на некоторых страницах все ок, на некоторых штамп начинается ставиться посредине странице. Выложить свой файл не могу...

Мне нейросеть написала, что нужно "жестко" растрировать страницы и после накладывать штамп.
 
  • Смешно
Реакции: ~RA~
Ну, нет так нет, никто не настаивает 'by'
 
Это правильно вставилась,
1779971183162.png
а после 12й странице началось сверху-посредине
1779971107068.png
 
Python:
import fitz
from PIL import Image
from tkinter import Tk, filedialog
import io

CM = 28.3465

# ===== НАСТРОЙКИ =====

# Отступы
MARGIN_LEFT = 1.5 * CM
MARGIN_BOTTOM = 1.5 * CM

# Размер печати
STAMP_W_CM = 7.19
STAMP_H_CM = 4.12

# DPI рендера
DPI = 200

# =====================

# cm -> px
def cm_to_px(cm_value, dpi):
    return int((cm_value / 2.54) * dpi)

STAMP_W = cm_to_px(STAMP_W_CM, DPI)
STAMP_H = cm_to_px(STAMP_H_CM, DPI)

MARGIN_X = cm_to_px(1.5, DPI)
MARGIN_Y = cm_to_px(1.5, DPI)

# Выбор файлов
root = Tk()
root.withdraw()

pdf_path = filedialog.askopenfilename(
    title="Выберите PDF",
    filetypes=[("PDF files", "*.pdf")]
)

stamp_path = filedialog.askopenfilename(
    title="Выберите PNG печати",
    filetypes=[("PNG files", "*.png")]
)

if not pdf_path or not stamp_path:
    print("Файл не выбран")
    exit()

# Загружаем печать
stamp = Image.open(stamp_path).convert("RGBA")
stamp = stamp.resize((STAMP_W, STAMP_H))

# Открываем PDF
doc = fitz.open(pdf_path)

# Новый PDF
new_pdf = fitz.open()

for page_num in range(len(doc)):

    page = doc[page_num]

    # Рендер страницы в bitmap
    pix = page.get_pixmap(dpi=DPI)

    img = Image.open(io.BytesIO(pix.tobytes("png"))).convert("RGBA")

    # Координаты снизу слева
    x = MARGIN_X
    y = img.height - STAMP_H - MARGIN_Y

    # Наложение печати
    img.paste(stamp, (x, y), stamp)

    # Сохраняем во временный PNG в памяти
    img_bytes = io.BytesIO()
    img.convert("RGB").save(img_bytes, format="JPEG", quality=95)

    # Добавляем страницу в новый PDF
    rect = fitz.Rect(0, 0, img.width, img.height)

    pdf_page = new_pdf.new_page(
        width=img.width,
        height=img.height
    )

    pdf_page.insert_image(
        rect,
        stream=img_bytes.getvalue()
    )

    print(f"Страница {page_num + 1} обработана")

# Сохранение
output = pdf_path.replace(".pdf", "_STAMPED.pdf")

new_pdf.save(output)

print("\nГотово:")
print(output)

Необходимые библиотеки и компоненты
pip install pymupdf pillow
 
Если все страницы +- одного размера, вообще проблем не вижу: переписать всё ps-pdf в один размер и после этого скормить программе.
 
Последнее редактирование:
Кроме того, оно может быть рататуевано на одной странице вправо, а на другой влево. А на третьей вообще вверх ногами будет. И что делать бедному рукивноги?
 
Поэтому не нужно заниматься самолечением. Просто Акробат, просто ватермарка
 
  • Спасибо
Реакции: densen
Вариантов тьма. Не понимаю мук выбора.