Конвертер изображений в PDF на Python — офлайн‑приложение с Tkinter, Pillow и ReportLab

Зачем нужен офлайн‑конвертер изображений в PDF
Конвертация изображений в PDF полезна для создания отчётов, портфолио, архивирования и отправки коллекций изображений в одном файле. Офлайн‑инструмент исключает необходимость загружать личные или конфиденциальные изображения в облачные сервисы — важный момент для безопасности и соответствия политике компании.
Кратко о подходе: используем стандартный GUI Tkinter для выбора и предпросмотра файлов, Pillow (PIL) для работы с изображениями и ReportLab для генерации PDF, при этом сохраняем исходные размеры изображений на страницах PDF.
Что потребуется и как установить
- Python 3.7+ (рекомендуется 3.8+).
- Tkinter — часто включён в дистрибутив Python, но на некоторых Linux‑системах его надо установить отдельно (пакет python3‑tk в Debian/Ubuntu). На macOS и Windows Tkinter обычно поставляется с Python.
- Pillow — библиотека для обработки изображений.
- ReportLab — генерация PDF документов.
Установка зависимостей:
- На Debian/Ubuntu: sudo apt install python3‑tk
- Установка Python‑пакетов:
pip install Pillow reportlabПримечание: pip install tkinter обычно не нужен и обычно не даёт полезного результата — лучше установить системный пакет, если Tkinter отсутствует.
Архитектура приложения и ключевые решения
Класс ImageToPDFConverter объединяет логику GUI и операции с изображениями:
- хранит список путей к выбранным изображениям;
- отображает миниатюры в окне предпросмотра (grid, 3 колонки по умолчанию);
- при конвертации открывает каждое изображение и создаёт страницу PDF с размерами, соответствующими изображению;
- использует reportlab.lib.utils.ImageReader, чтобы ReportLab корректно работал с разными форматами;
- предоставляет обработку ошибок и уведомления.
Главные проектные решения и причины:
- сохранять исходные размеры изображений на страницах PDF (важно для точного представления и печати);
- масштабировать только миниатюры в интерфейсе, а не оригинальные файлы;
- избегать сетевых сервисов по соображениям приватности.
Исправленный и расширенный рабочий пример кода
Ниже приведён полный пример приложения с улучшенной обработкой ошибок, поддержкой ImageReader и базовой проверкой DPI/конверсии единиц, чтобы PDF‑страницы корректно соответствовали размерам изображений.
import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
class ImageToPDFConverter:
def __init__(self, root):
self.root = root
self.image_paths = []
self.root.title("Image to PDF Converter")
self.root.geometry("900x700")
# Верхняя панель с кнопками
toolbar = tk.Frame(self.root)
toolbar.pack(fill=tk.X, pady=8)
self.select_images_button = tk.Button(toolbar, text="Выбрать изображения", command=self.select_images, font=("Helvetica", 12))
self.select_images_button.pack(side=tk.LEFT, padx=6)
self.convert_to_pdf_button = tk.Button(toolbar, text="Конвертировать в PDF", command=self.convert_to_pdf, font=("Helvetica", 12))
self.convert_to_pdf_button.pack(side=tk.LEFT, padx=6)
self.clear_button = tk.Button(toolbar, text="Очистить список", command=self.clear_selection, font=("Helvetica", 12))
self.clear_button.pack(side=tk.LEFT, padx=6)
# Метка состояния
self.status_label = tk.Label(self.root, text="Выберите изображения для конвертации", font=("Helvetica", 11))
self.status_label.pack(pady=6)
# Фрейм предпросмотра с полосой прокрутки
preview_outer = tk.Frame(self.root)
preview_outer.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
canvas_frame = tk.Canvas(preview_outer)
scrollbar = ttk.Scrollbar(preview_outer, orient=tk.VERTICAL, command=canvas_frame.yview)
self.preview_container = tk.Frame(canvas_frame)
self.preview_container.bind(
"", lambda e: canvas_frame.configure(scrollregion=canvas_frame.bbox("all"))
)
canvas_frame.create_window((0, 0), window=self.preview_container, anchor="nw")
canvas_frame.configure(yscrollcommand=scrollbar.set)
canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Размер миниатюр
self.thumb_width = 150
self.thumb_height = 150
def select_images(self):
files = filedialog.askopenfilenames(initialdir="/", title="Выберите изображения",
filetypes=(("Image files", "*.jpg *.jpeg *.png *.bmp *.tiff"), ("All files", "*.*")))
if not files:
return
self.image_paths = list(files)
self.status_label.config(text=f"Выбрано {len(self.image_paths)} файла(ов)")
self.render_preview()
def render_preview(self):
# Очистка предыдущих миниатюр
for widget in self.preview_container.winfo_children():
widget.destroy()
for i, image_path in enumerate(self.image_paths):
try:
img = Image.open(image_path)
thumb = self.resize_image(img, width=self.thumb_width, height=self.thumb_height)
photo = ImageTk.PhotoImage(thumb)
lbl = tk.Label(self.preview_container, image=photo)
lbl.image = photo
lbl.grid(row=i // 3, column=i % 3, padx=8, pady=8)
# подпись с именем файла
name = os.path.basename(image_path)
tk.Label(self.preview_container, text=name, wraplength=self.thumb_width).grid(row=(i // 3)+1, column=i % 3)
except Exception as e:
tk.Label(self.preview_container, text=f"Ошибка: {e}").grid(row=i // 3, column=i % 3)
def resize_image(self, image, width, height):
aspect_ratio = min(width / float(image.size[0]), height / float(image.size[1]))
new_width = int(aspect_ratio * image.size[0])
new_height = int(aspect_ratio * image.size[1])
return image.resize((new_width, new_height), resample=Image.Resampling.BILINEAR)
def clear_selection(self):
self.image_paths = []
self.status_label.config(text="Список очищен")
for widget in self.preview_container.winfo_children():
widget.destroy()
def convert_to_pdf(self):
if not self.image_paths:
messagebox.showwarning("Нет файлов", "Сначала выберите изображения для конвертации")
return
pdf_path = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=(("PDF Files", "*.pdf"),), title="Сохранить PDF как")
if not pdf_path:
return
try:
c = canvas.Canvas(pdf_path)
for image_path in self.image_paths:
img = Image.open(image_path)
# Получаем размеры изображения в пикселях
width_px, height_px = img.size
# Попытка получить DPI, по умолчанию 72
dpi = img.info.get('dpi', (72, 72))[0] if isinstance(img.info.get('dpi', None), tuple) else img.info.get('dpi', 72)
if not dpi:
dpi = 72
# Конвертация пикселей в пункты ReportLab (1 пункт = 1/72 дюйма)
width_pt = width_px * 72.0 / dpi
height_pt = height_px * 72.0 / dpi
c.setPageSize((width_pt, height_pt))
img_reader = ImageReader(image_path)
c.drawImage(img_reader, 0, 0, width=width_pt, height=height_pt)
c.showPage()
c.save()
messagebox.showinfo("Успех", f"PDF сохранён по пути: {pdf_path}")
except Exception as e:
messagebox.showerror("Ошибка при конвертации", str(e))
if __name__ == "__main__":
root = tk.Tk()
app = ImageToPDFConverter(root)
root.mainloop() Важно: код пытается учитывать DPI изображения и переводит пиксели в «пункты» (points) ReportLab для корректного соотношения размеров в PDF. Если у изображения не задан DPI, применяется 72 dpi по умолчанию.
Пояснения к важным частям кода
- Preview: миниатюры генерируются отдельно, чтобы не изменять исходные файлы. Для масштабирования миниатюр используется bilinear‑интерполяция для баланса качества и производительности.
- drawImage через ImageReader: ReportLab корректно обрабатывает источники через ImageReader, особенно для PNG с альфа‑каналом.
- setPageSize: перед рисованием страницы мы явным образом устанавливаем размер страницы равным размеру изображения в пунктах.
- DPI: если у изображения есть мета‑информация DPI, мы учитываем её. Это важно для соответствия физическим размерам при печати.
Когда этот подход не подойдёт
- Когда нужно создать единый PDF с несколькими изображениями на одной странице — текущая реализация создаёт одну страницу на изображение.
- Для массовой пакетной обработки тысяч изображений может потребоваться оптимизация по памяти и потоковая обработка (например, генерация PDF частями).
- При необходимости минимизации размера PDF (компрессия/оптимизация) потребуются дополнительные шаги: перекодирование изображений, понижение качества JPEG, использование специализированных библиотек.
Альтернативные подходы и библиотеки
- img2pdf — библиотека для безпотерьной упаковки JPEG/PNG в PDF; часто производительнее для простых задач.
- fpdf / FPDF2 — лёгкая библиотека для генерации PDF, проще синтаксис, но меньше возможностей по работе с графикой.
- PyPDF2 / pikepdf — полезны для операций над готовыми PDF: слияние, разбиение, метаданные.
Выбор зависит от задачи: если нужно просто «обернуть» изображения в PDF, img2pdf будет быстрее и даёт контроль над качеством. Если нужен сложный шаблон с текстом, таблицами и графиками — ReportLab даёт больше контроля.
Руководство по тестированию и критерии приёмки
Критерии приёмки:
- Приложение корректно принимает набор JPG/PNG/BMP/TIFF и создаёт PDF.
- Каждое изображение сохраняется на отдельной странице без искажения пропорций.
- Миниатюры в предпросмотре отображаются корректно и не ломают UI при большом количестве файлов.
- При отмене диалогов приложение не падает.
- Сообщения об ошибках понятны пользователю.
Тестовые сценарии:
- Один файл JPG 1920×1080, DPI 72 — PDF страница соответствует размеру изображения.
- Несколько файлов разных форматов и размеров — все страницы корректно расположены.
- Файл с отсутствующим правом чтения — приложение отображает понятную ошибку и продолжает.
- Большая папка (>200 файлов) — проверка производительности и утечек памяти.
Советы по улучшению и продвинутые фичи
- Пакетная обработка в фоновом потоке (threading или multiprocessing) с индикатором прогресса.
- Опция «уместить на страницу» vs «сохранить исходный размер».
- Возможность собирать несколько миниатюр на одной странице (n‑up layout).
- Добавление водяных знаков или нумерации страниц с помощью ReportLab.
- Опции сжатия: перекодирование изображений в JPEG с контролем качества перед вставкой в PDF.
Безопасность и приватность
- Приложение работает локально, изображения не отправляются в сеть.
- Следует обрабатывать исключения и проверять пути, чтобы избежать перезаписи системных файлов.
- Если добавляете функцию экспорта/обмен файлами, подумайте о временных файлах и их безопасном удалении.
Совместимость и переносимость
- На Linux убедитесь, что установлен python3‑tk; на Windows/macOS Tkinter обычно поставляется с Python.
- Pillow и ReportLab работают на основных платформах.
- Тестируйте поведение DPI и ориентации изображений (EXIF rotation для камер — Pillow умеет читать и применять поворот через ImageOps.exif_transpose).
Короткое объявление для пользователей (анонс)
Готов офлайн‑конвертер изображений в PDF на Python: быстрый GUI для выбора изображений, предпросмотр и сохранение коллекции в единый PDF без загрузки файлов в облако. Подходит для портфолио, отчётов и защищённой работы с изображениями.
Предложение для соцсетей
OG title: Конвертер изображений в PDF на Python — офлайн OG description: Соберите офлайн‑конвертер изображений в PDF с Tkinter, Pillow и ReportLab — предпросмотр, сохранение размеров и приватность.
Краткая шпаргалка и чек‑лист для запуска
Перед запуском:
- Установите Python 3.7+.
- На Linux: sudo apt install python3‑tk
- Установите пакеты: pip install Pillow reportlab
- Запустите скрипт python your_script.py
Роли и минимальный чек‑лист:
- Разработчик: проверить установку зависимостей, покрыть тестами кейсы с разными форматами.
- Тестировщик: прогнать тестовые сценарии (см. выше).
- Пользователь: выбрать файлы, проверить PDF в просмотрщике, убедиться в сохранении качества.
Короткая глоссарий на одну строку
- DPI — плотность точек на дюйм, влияет на соответствие пикселей реальным размерам при печати.
- ImageReader — адаптер ReportLab для корректной вставки изображений.
- setPageSize — метод ReportLab для установки размера страницы в пунктах.
Заключение
Этот материал даёт готовую, расширяемую реализацию офлайн‑конвертера изображений в PDF на Python, учитывающую практические нюансы (миниатюры, DPI, обработка ошибок). В зависимости от требований можно заменить ReportLab на более лёгкий инструмент для упаковки изображений в PDF или добавить продвинутые функции компрессии и макетов.
Important: если ваши изображения содержат EXIF‑поворот (например, сделаны камерой в портретном режиме), используйте Pillow ImageOps.exif_transpose перед отрисовкой, чтобы сохранить ориентацию.
Резюме:
- Офлайн‑конвертер полезен для приватной работы с изображениями.
- Используйте Pillow для обработки и ReportLab для генерации PDF; учитывайте DPI.
- Для массовой обработки рассмотрите альтернативы (img2pdf) и оптимизацию по памяти.