Создание и изменение PDF-файлов в Python. Часть 2

Оглавление

Объединение и слияние PDF-файлов

Существуют две похожие задачи — слияние и объединение нескольких PDF-файлов в один.

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

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

В этой секции мы изучим, как объединять и сливать PDF-файлы, используя класс PdfFileMerger из пакета PyPDF2.

Используем класс PdfFileMerger

Класс PdfFileMerger во многом очень похож на класс PdfFileWriter, который мы изучили в предыдущем разделе. Для записи PDF-файлов можно использовать оба эти класса. В обоих случаях мы добавляем страницы к экземплярам класса, а затем записываем их в файл.

Основное отличие между этими двумя классами состоит в следующем: класс PdfFileWriter может только добавлять страницы или файлы в конец существующего файла, в то время как класс PdfFileMerger может вставлять их в любое место.

Что же, двинемся дальше и создадим наш первый экземпляр класса PdfFileMerger. Для этого вначале импортируем этот класс, а потом создадим его экземпляр класса, сохранив его в переменную pdf_merger:

from PyPDF2 import PdfFileMerger
pdf_merger = PdfFileMerger()

Объекты класса PdfFileMerger создаются пустыми. Прежде чем с этими объектами можно будет что-то делать, к ним надо добавить одну или несколько страниц.

Есть два способа добавить страницы в объект pdf_merger, и какой из них выбрать, зависит от того, что нам нужно сделать:

  • Метод .append() добавляет все страницы, находящиеся в PDF-файле, после последней страницы в нашем объекте класса PdfFileMerger
  • Метод .merge() вставляет все страницы, находящиеся в PDF-файле, после определенной странице в объекте класса PdfFileMerger

В данной секции мы рассмотрим оба этих способа, начав с метода .append().

Объединяем PDF-файлы при помощи метода .append()

В папке practice_files/ нашего репозитория есть еще одна папка под названием expense_reports. В ней содержатся три файла, каждый из которых представляет собой отчет о расходах сотрудника по имени Peter Python.

Питер должен объединить эти три PDF-файла в один и представить их своему работодателю в качестве отчета для получения компенсации за некоторые связанные с работой расходы.

Начнем, как обычно, с модуля pathlib и получим список объектов класса Path для каждого из трех отчетов о расходах, лежащих в папке expense_reports:

from pathlib import Path
reports_dir = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "expense_reports"
)

После импорта класса Path мы построили путь до наших файлов в папке expense_reports. Обратите внимание, что вам возможно придется скорректировать этот путь в соответствии с расположением файлов на вашем компьютере.

Как только мы создали путь к каталогу cost_reports/ и сохранили его в переменную reports_dir, мы можем использовать метод .glob () для получения итерируемой последовательности путей ко всем PDF-файлам в этом каталоге.

Давайте посмотрим, что есть в этой директории:

for path in reports_dir.glob("*.pdf"):
    print(path.name)
...
Expense report 1.pdf
Expense report 3.pdf
Expense report 2.pdf

Имена всех трех файлов выведены на экран, но, как можно заметить, не по порядку. Более того, их порядок на вашем компьютере также может отличаться от того, что вы видите здесь.

Таким образом, метод .glob() не обеспечивает порядок данных, которые мы сохранили в список. Но мы можем упорядочить их самостоятельно, использовав стандартный метод списков .sort().

expense_reports = list(reports_dir.glob("*.pdf"))
expense_reports.sort()

Заметим, что метод .sort() изменяет список по месту (in-place метод), и поэтому не нужно сохранять результат в отдельную переменную. После вызова метода .sort() список expense_reports будет отсортирован в алфавитном порядке.

Чтобы убедиться в этом, выведем опять этот список на экран:

for path in expense_reports:
    print(path.name)
...
Expense report 1.pdf
Expense report 2.pdf
Expense report 3.pdf

Выглядит отлично!

Теперь мы можем приступить к объединению всех трех файлов. Для этого будем использовать метод PdfFileMerger.append(). Он требует всего один строковый аргумент, в котором содержится путь к PDF-файлу. Когда мы вызываем метод .append (), все страницы в PDF-файле добавляются к набору страниц, которые уже хранятся в объекте класса PdfFileMerger.

Теперь проверим это в действии. Для начала импортируем класс PdfFileMerger и создадим его новый экземпляр:

from PyPDF2 import PdfFileMerger
pdf_merger = PdfFileMerger()

Теперь переберем все пути в отсортированном списке expense_reports и добавим их в pdf_merger:

for path in expense_reports:
    pdf_merger.append(str(path))

Обратите внимание, что каждый объект Path в списке expense_reports преобразуется в строку при помощи встроенной функции str() перед передачей в метод pdf_merger.append ().

Когда все PDF-файлы из директории expense_reports/ объединены в объекте pdf_merger, нам остается только записать этот объект в PDF-файл. У экземпляра класса PdfFileMerger имеется метод .write(), который работает также как метод PdfFileWriter.write().

Открываем новый файл в режиме записи и передаем его в метод pdf_merge.write():

with Path("expense_reports.pdf").open(mode="wb") as output_file:
    pdf_merger.write(output_file)

Теперь у нас в нашей текущей директории есть файл под названием expense_reports.pdf. Открыв его, можно убедится, что все три отчета о затратах объединены в одном PDF-файле.

Слияние PDF-файлов при помощи метода .merge()

Чтобы объединить два или более PDF-файла произвольным образом, используется метод .merge(). Этот метод похож на метод append(), за исключением того, что в нем вы обязаны определить, в какое место итогового файла будет помещено содержимое объединяемых файлов.

Рассмотрим следующий пример. В компании Goggle, Inc. был подготовлен квартальный отчет, но в него забыли включить оглавление. Peter Python быстро заметил эту ошибку и создал PDF-файл с недостающим оглавлением. Теперь нужно объединить этот файл с файлом отчета правильным образом.

Оба этих файла находятся в нашем репозитории в папке quarterly_report/, которая в свою очередь находится в папке practice_files. Отчет находится в файле с именем report.pdf, а оглавление — в файле с именем toc.pdf.

Итак, импортируем класс PdfFileMerger и создадим объект Path для двух наших файлов:

from pathlib import Path
from PyPDF2 import PdfFileMerger
report_dir = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "quarterly_report"
)
report_path = report_dir / "report.pdf"
toc_path = report_dir / "toc.pdf"

Первым делом давайте присоединим файл с отчетом к только что созданному экземпляру класса PdfFileMerger, используя метод .append():

pdf_merger = PdfFileMerger()
pdf_merger.append(str(report_path))

Теперь, когда в pdf_merger есть все страницы отчета, мы можем вставить оглавление в нужное место. Если вы откроете файл report.pdf с помощью любой программы чтения PDF, то увидите, что первая страница отчета является титульной страницей. Вторая — введение, а остальные страницы содержат различные разделы отчета.

Мы хотим вставить оглавление после титульной страницы, прямо перед введением. Поскольку индексация страниц PDF-файлов в пакете PyPDF2 начинаются с 0, нам необходимо вставить оглавление после страницы с индексом 0 и перед страницей с индексом 1.

Чтобы это сделать, вызовем метод pdf_merger.merge() с двумя аргументами:

  1. В первом — целое число 1 указывает индекс страницы, в которую мы хотим вставить наше оглавление.
  2. Второй аргумент содержит путь, по которому можно найти файл с оглавлением.

Вот как это выглядит:

pdf_merger.merge(1, str(toc_path))

Все страницы из файла toc.pdf будут вставлены перед страницей с индексом 1. Так как этот файл состоит только из одной страницы, она будет иметь в новом файле индекс 1. Соответственно, страница, у которой был индекс 1 до этого, получит индекс 2. Индекс последующих страниц также увеличится на единицу.

Теперь сохраним получившийся PDF-файл:

with Path("full_report.pdf").open(mode="wb") as output_file:
    pdf_merger.write(output_file)

Теперь в нашей рабочей директории есть файл под названием ull_report.pdf. Открыв его в любом редакторе PDF, можно убедится, что оглавление вставлено на свое место.

Объединение и слияние PDF-файлов — обычные операции. Хотя примеры в этом разделе, по общему признанию, несколько надуманы, вы можете себе представить, насколько полезной была бы программа для объединения тысяч PDF-файлов или для автоматизации рутинных задач, которые в противном случае заняли бы очень много времени.

Проверка ваших знаний

Упражнение: объедините два PDF-файла

В директории practice_files/ нашего репозитория есть два файла merge1.pdf и merge2.pdf.

Используя экземпляр класса PdfFileMerge, объедините два этих файла при помощи метода.append().  Результат запишите в новый файл под названием concatenated.pdf.

Решение: объединение двух PDF-файлов

Устанавливаем путь к нашему PDF-файлу:

# Сначала импортируем необходимые классы и библиотеки
from pathlib import Path
from PyPDF2 import PdfFileMerger

# Создаем объект Path, который содержит путь до нужного файла.
# На вашем компьютере он может быть другим
BASE_PATH = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
)

pdf_paths = [BASE_PATH / "merge1.pdf", BASE_PATH / "merge2.pdf"]

Теперь создаем экземпляр класса PdfFileMerger:

pdf_merger = PdfFileMerger()

Далее мы при помощи цикла переберем все значения переменной pdf_paths и при помощи метода .append() добавим каждый файл в объект pdf_merger:

for path in pdf_paths:
    pdf_merger.append(str(path))

И наконец, запишем содержимое pdf_merger в новый файл под названием concatenated.pdf:

output_path = Path.home() / "concatenated.pdf"
with output_path.open(mode="wb") as output_file:
    pdf_merger.write(output_file)

Создание PDF-файла с нуля

Пакет PyPDF2 отлично подходит для чтения и изменения существующих PDF-файлов, но у него есть одно серьезное ограничение: его нельзя использовать для создания нового файла формата PDF. Поэтому в этом разделе для создания PDF-файлов мы будем использовать ReportLab Toolkit.

ReportLab — это полнофункциональное решение для создания PDF-файлов. Существует платная коммерческая версия, но также доступна версия с открытым исходным кодом с ограниченными возможностями.

Устанавливаем reportlab

Для начала установим пакет reportlab при помощи менеджера pip.

$ python3 -m pip install reportlab

Проверим установку при помощи команды pip show:

$ python3 -m pip show reportlab

Name: reportlab
Version: 3.5.34
Summary: The Reportlab Toolkit
Home-page: http://www.reportlab.com/
Author: Andy Robinson, Robin Becker, the ReportLab team
        and the community
Author-email: reportlab-users@lists2.reportlab.com
License: BSD license (see license.txt for details),
         Copyright (c) 2000-2018, ReportLab Inc.
Location: c:\users\davea\venv\lib\site-packages
Requires: pillow
Required-by:

На момент написания статьи последней версией пакета reportlab была 3.5.34. Если вы используете IDE IDLE, для работы с только что установленным пакетом ее надо перезапустить.

Используем класс Canvas

Основным интерфейсом для создания PDF-файлов при помощи пакета reportlab является класс Canvas, который находится в модуле reportlab.pdfgen.canvas.

Импортируем этот класс:

from reportlab.pdfgen.canvas import Canvas

Когда мы создаем новый экземпляр класса Canvas, нам нужно указать строку с именем файла PDF, который мы создаем. Например, создадим новый экземпляр класса Canvas для файла hello.pdf:

canvas = Canvas("hello.pdf")

Теперь у нас есть экземпляр класса Canvas, который мы сохранили в переменную canvas. Этот экземпляр класса связан с файлом в текущем рабочем каталоге hello.pdf. Однако сам файл hello.pdf еще не существует.

Давайте добавим текст в этот PDF-файл. Для этого воспользуемся методом drawString ():

canvas.drawString(72, 72, "Hello, World")

Первые два аргумента, переданные в метод .drawString (), определяют расположение текста на странице. Первый аргумент указывает расстояние от левого края страницы, а второй — расстояние от нижнего края.

Значения, передаваемые в метод .drawString (), измеряются в точках. Поскольку точка равна 1/72 дюйма, .drawString (72, 72, «Hello, World») рисует строку «Hello, World» на расстоянии одного дюйма слева и одного дюйма от нижней части страницы.

Чтобы сохранить наш PDF-файл, используем метод .save():

canvas.save()

Теперь в нашей рабочей директории есть файл под названием hello.pdf. Его можно открыть и увидеть внизу страницы надпись «Hello, World»!

Есть два момента, которые хотелось бы отметить:

  1. Формат страницы по умолчанию — А4.
  2. По умолчанию используется шрифт Helvetica c размером 12.

Но разумеется, вы не обязаны эти настройки оставлять.

Устанавливаем размер страницы

При создании экземпляра класса Canvas можно изменить размер страницы с помощью необязательного параметра pagesize. Этот параметр принимает кортеж из двух чисел типа float, представляющих ширину и высоту страницы в точках.

Например, чтобы установить размер страницы 8,5 дюймов в ширину и 11 дюймов в высоту, нужно создать следующий объект canvas:

canvas = Canvas("hello.pdf", pagesize=(612.0, 792.0))

612 — это 8.5*72, а 792 — это 11*72.

Если перевод точек в дюймы или сантиметры — не ваш конек, в этой библиотеке есть модуль reportlab.lib.units, который может вам помочь. Он содержит несколько вспомогательных объектов, таких как inch и cm, которые упростят ваши вычисления.

Давайте их импортируем:

from reportlab.lib.units import inch, cm

Теперь мы можем посмотреть, что они из себя представляют:

cm
# результат: 28.346456692913385
inch
# результат: 72.0

И cm, и inch принимают значения с плавающей точкой. Они представляют собой количество точек, содержащихся в них. Соответственно, в сантиметре 28.346456692913385 точки, а в дюйме ровно 72.

Чтобы это использовать, их надо просто умножать на то, что мы хотим преобразовать в точки. Например, вот как использовать дюйм, чтобы установить размер страницы 8,5 дюймов в ширину и 11 дюймов в высоту:

canvas = Canvas("hello.pdf", pagesize=(8.5 * inch, 11 * inch))

Передавая кортеж в параметр pagesize, можно создать абсолютно любой размер страницы. Однако в пакете reportlab есть несколько стандартных встроенных размеров, с которыми проще работать.

Размеры страниц находятся в модуле reportlab.lib.pagesizes. Например, чтобы установить размер страницы letter, можно импортировать объект LETTER из модуля reportlab.lib.pagesizes и передать его параметру pagesize при создании экземпляра класса Canvas:

from reportlab.lib.pagesizes import LETTER
canvas = Canvas("hello.pdf", pagesize=LETTER)

Изучив объект LETTER, можно увидеть, что он хранит в себе кортеж из двух чисел с плавающей точкой:

LETTER
# результат: (612.0, 792.0)

Модуль reportlab.lib.pagesize содержит много стандартных размеров страниц. Вот несколько примеров с их размерами:

 Страница Размеры
A4 210 mm x 297 mm
LETTER 8.5 in x 11 in
LEGAL 8.5 in x 14 in
TABLOID 11 in x 17 in

В завершение заметим, что данный модуль содержит в себе все размеры, которые есть в стандарте ISO 216.

Устанавливаем свойства шрифтов

При работе с текстом в Canvas можно менять сам шрифт, размер шрифта и цвет шрифта.

Чтобы изменить шрифт и размер шрифта, можно использовать метод .setFont (). Для начала создадим новый экземпляр класса Canvas с именем файла font-example.pdf и размером страницы LETTER:

canvas = Canvas("font-example.pdf", pagesize=LETTER)

Затем установиv шрифт Times New Roman размером в 18 точек:

canvas.setFont("Times-Roman", 18)

Наконец, напишем на странице строку «Times New Roman (18 pt)» и сохраним ее:

canvas.drawString(1 * inch, 10 * inch, "Times New Roman (18 pt)")
canvas.save()

С этими настройками текст будет написан на расстоянии одного дюйма с левой стороны страницы и десяти дюймов снизу. Можно открыть файл font-example.pdf в текущем рабочем каталоге и убедиться в этом!

По умолчанию доступно три шрифта:

  1. «Courier»
  2. «Helvetica»
  3. «Times-Roman»

Каждый может быть в виде жирного шрифта либо курсива. Вот список всех возможных вариантов:

  • «Courier»
  • «Courier-Bold
  • «Courier-BoldOblique»
  • «Courier-Oblique»
  • «Helvetica»
  • «Helvetica-Bold»
  • «Helvetica-BoldOblique»
  • «Helvetica-Oblique»
  • «Times-Bold»
  • «Times-BoldItalic
  • «Times-Italic»
  • «Times-Roman»

Установить цвет шрифта можно при помощи метода .setFillColor(). В следующем примере мы создаем PDF-файл под названием font-colors.pdf и пишем в нем текст синим цветом:

from reportlab.lib.colors import blue
from reportlab.lib.pagesizes import LETTER
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas

canvas = Canvas("font-colors.pdf", pagesize=LETTER)

# Устанавливаем шрифт Times New Roman с 12 размером
canvas.setFont("Times-Roman", 12)

# Пишем синий текст на расстоянии 1 дюйма слева и 
# 10 дюймов снизу
canvas.setFillColor(blue)
canvas.drawString(1 * inch, 10 * inch, "Blue text")

# Сохраняем PDF-файл
canvas.save()

blue — это объект, импортированный из модуля reportlab.lib.colors. Этот модуль содержит несколько обычных цветов. Полный список цветов можно найти в исходном коде пакета reportLab.

В примерах этого раздела освещены основы работы с объектами класса Canvas. Но мы затронули только то, что лежит на поверхности. С помощью reportlab можно с нуля создавать таблицы, формы и даже высококачественную графику!

Руководство пользователя ReportLab содержит множество примеров создания документов PDF с нуля. Это самое правильное место для начала изучения данного пакета.

Проверка ваших знаний

Упражнение: создайте PDF-файл с нуля

Создайте PDF-файл в рабочей директории вашего компьютера под названием realpython.pdf с размером страницы LETTER. Страница файла должна содержать текст «Hello, Real Python!», размещённый в двух дюймах от левого края и в восьми дюймах от нижнего края страницы.

Решение:

Создадим экземпляр класса Canvas с размеров страниц LETTER:

from reportlab.lib.pagesizes import LETTER
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas

canvas = Canvas("font-colors.pdf", pagesize=LETTER)

Теперь напишем строку «Hello, Real Python!» на расстоянии два дюйма слева и восемь дюймов снизу:

canvas.drawString(2 * inch, 8 * inch, "Hello, Real Python!")

И, наконец, сохраняем все это для записи в PDF-файл:

canvas.save()