Перевод статьи Create and Modify PDF Files in Python.

Умение создавать и изменять PDF файлы в Python очень полезный и нужный навык. Тем более, что PDF, или Portable Document Format, является одним из наиболее распространенных форматов документов, предназначенный в том числе и для обмена текстовыми данными через сеть Интернет. PDF файлы могут содержать в файле как обычный текст, так и изображения, таблицы, формы и мультимедиа, например, видео.

Такое обилие типов контента, которые могут содержаться в одном файле существенно затрудняет работу с форматом PDF. Так как при открытии файла и последующей его обработке мы имеем дело с большим количеством разнородных типов данных, которые нужно декодировать. К счастью, в экосистеме языка Python есть немало отличных пакетов для чтения, обработки и создания файлов в формате PDF.

В этом руководстве вы узнаете, как:

  • Читать текст из PDF файла,
  • Разделить PDF файл на несколько отдельных файлов,
  • Объединить и слить содержимое PDF файлов в нужном вам порядке,
  • Повернуть и обрезать страницы в PDF файле,
  • Шифровать и дешифровать содержимое PDF файлов с паролем,
  • Создать PDF файл с нуля.

Понять материал этого руководства наилучшим образом помогут примеры. Вы можете скачать материалы, использованные в примерах, перейдя по ссылке ниже: учебные материалы.

Извлекаем из PDF файла текст

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

$ python3 -m pip install PyPDF2

Проверим установился ли пакет в нашей системе, выполнив следующую команду в терминале:

$ python3 -m pip show PyPDF2
Name: PyPDF2
Version: 1.26.0
Summary: PDF toolkit
Home-page: http://mstamy2.github.com/PyPDF2
Author: Mathieu Fenniak
Author-email: biziqe@mathieu.fenniak.net
License: UNKNOWN
Location: c:\\users\\david\\python38-32\\lib\\site-packages
Requires:
Required-by:

Обратите внимание на информацию о версии пакета. На момент написания этой статьи, последняя версия PyPDF2 была 1.26.0. Если у вас открыто интерактивное окно IDLE, то прежде чем вы сможете использовать пакет PyPDF2, вам необходимо перезапустить его.

Открываем PDF файл

Для начала откроем PDF файл и прочитаем о нем некоторую сводную информацию. Для демонстрации работы кода пример будем использовать файл Pride_and_Prejudice.pdf, расположенный в папке practice_files/ в репозитории с примерами к этому руководству.

Откройте интерактивное окно IDLE и импортируйте класс PdfFileReader из пакета PyPDF2:

from PyPDF2 import PdfFileReader

Для того, чтобы создать новый экземпляр класса PdfFileReader, нам понадобится указать путь к PDF файлу, который мы хотим открыть. Давайте получим его, используя для этого модуль pathlib:

from pathlib import Path
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)

Переменная pdf_path теперь содержит путь к PDF версии книги Джейн Остин Гордость и предубеждение .

Примечание. Возможно, потребуется изменить путь к вашему PDF файлу, который содержится в переменной pdf_path, чтобы он соответствовал расположению папки creating-and-modifying-pdfs/ на вашем компьютере.

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

pdf = PdfFileReader(str(pdf_path))

Конвертируем содержимое переменной pdf_path в строку, так как класс PdfFileReader не умеет работать с объектом pathlib.Path.

Как вы уже наверное знаете, что все открытые файлы должны быть закрыты до завершения работы кода вашей программы. Экземпляр класса PdfFileReader сделает это для нас, по умолчанию,поэтому в дальнейшем вам не следует беспокоиться об открытии или закрытии нашего PDF файла!

Теперь, когда вы создали новый экземпляр класса PdfFileReader, вы можете использовать его для сбора общей информации о нашем PDF файле. Например, метод getNumPages() возвращает количество страниц, содержащихся в PDF документе:

>>> pdf.getNumPages()
234

Обратите внимание, что имя метода getNumPages() написано в формате mixedCase, а не lower_case_with_underscores, как рекомендуется в PEP 8. Это связано с тем, что PEP 8 — это лишь набор рекомендаций для оформления кода, а не правил работы языковых конструкций Python. Что касается языка Python, формат именования идентификаторов в стиле mixedCase вполне приемлем.

Примечание: Код пакета PyPDF2 был адаптирован из пакета pyPdf, который был разработан в 2005 году, всего через четыре года после публикации PEP 8. В то время многие программисты на Python мигрировали с других языков программирования, в которых стиль написания кода в mixedCase был наиболее распространен.

Вы можете получить доступ к сводной информации о документе, используя атрибут .documentInfo:

>>> pdf.documentInfo
{'/Title': 'Pride and Prejudice, by Jane Austen', '/Author': 'Chuck',
'/Creator': 'Microsoft® Office Word 2007',
'/CreationDate': 'D:20110812174208', '/ModDate': 'D:20110812174208',
'/Producer': 'Microsoft® Office Word 2007'}

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

Получим заголовок документа, используя атрибут title:

>>> pdf.documentInfo.title
'Pride and Prejudice, by Jane Austen'

Объект documentInfo также содержит метаданные документа PDF, содержимое которых задается при создании файла PDF документа.

Класс PdfFileReader предоставляет все необходимые методы и атрибуты, которые необходимы для получения доступа к содержимому PDF файла. Давайте рассмотрим, что вы можете сделать с PDF файлом, используя возможности пакета PyPDF2, и как это сделать.

Извлекаем текстовое содержимое со страницы документа

Страницы PDF документа представлены в PyPDF2 классом PageObject. Для взаимодействия с определенными страницами документа используете отдельные экземпляры класса PageObject. При этом вам не нужно создавать новые объекта экземпляров класса PageObject непосредственно. Вы можете получить к ним доступ, используя метод getPage() класса PdfFileReader.

Таким образом, для извлечения текста с определенной страницы PDF документа необходимо сделать два шага:

  1. Получить нужный объект типа PageObject из экземпляра класса, используя метод PdfFileReader.getPage().
  2. Извлечь текст (в виде строки) со страницы с помощью метода extractText() класса PageObject.

И так, в файле Pride_and_Prejudice.pdf 234 страницы. Соответственно каждая из страниц имеет индекс между 0 и 233. Вы можете получить представление нужной страницы в виде объекта PageObject, передав ее индекс в качестве аргумента при вызове метода PdfFileReader.getPage():

first_page = pdf.getPage(0)

Проверим, что метод getPage() возвращает объект типа PageObject:

>>> type(first_page)
<class 'PyPDF2.pdf.PageObject'>

Извлечь текст страницы можно с помощью метода PageObject.extractText():

>>> first_page.extractText()
'\\n \\nThe Project Gutenberg EBook of Pride and Prejudice, by Jane
Austen\\n \\n\\nThis eBook is for the use of anyone anywhere at no cost
and with\\n \\nalmost no restrictions whatsoever.  You may copy it,
give it away or\\n \\nre\\n-\\nuse it under the terms of the Project
Gutenberg License included\\n \\nwith this eBook or online at
www.gutenberg.org\\n \\n \\n \\nTitle: Pride and Prejudice\\n \\n
\\nAuthor: Jane Austen\\n \\n \\nRelease Date: August 26, 2008
[EBook #1342]\\n\\n[Last updated: August 11, 2011]\\n \\n \\nLanguage:
Eng\\nlish\\n \\n \\nCharacter set encoding: ASCII\\n \\n \\n***
START OF THIS PROJECT GUTENBERG EBOOK PRIDE AND PREJUDICE ***\\n \\n
\\n \\n \\n \\nProduced by Anonymous Volunteers, and David Widger\\n
\\n \\n \\n \\n \\n \\n \\nPRIDE AND PREJUDICE \\n \\n \\nBy Jane
Austen \\n \\n\\n \\n \\nContents\\n \\n'

Обратите внимание, что выведенные в консоли данные отформатированы для лучшего восприятия на странице (содержимое отформатировано по определенному размеру страницы). Таким образом вид форматирования данных, которые вы видите на экране вашего компьютера, может быть различным.

У каждого объекта типа PdfFileReader есть атрибут pages, который вы можете использовать для перебора всех страниц в PDF документе по порядку.

Например, в следующем примере в цикле for по порядку выводится текстовое содержимое каждой страницы PDF файла:

for page in pdf.pages:
    print(page.extractText())

Теперь объединим все, что вы узнали, и напишем программу, которая извлекает весь текст из файла Pride_and_Prejudice.pdf и сохраняет его в новый файл с расширением .txt.

Собираем все вместе

Откройте новое интерактивное окно IDLE и введите в него следующий код:

from pathlib import Path
from PyPDF2 import PdfFileReader

# Измените путь к файлу для корректной работы на вашем компьютере
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice-files"
    / "Pride_and_Prejudice.pdf"
)

# 1
pdf_reader = PdfFileReader(str(pdf_path))
output_file_path = Path.home() / "Pride_and_Prejudice.txt"

# 2
with output_file_path.open(mode="w") as output_file:
    # 3
    title = pdf_reader.documentInfo.title
    num_pages = pdf_reader.getNumPages()
    output_file.write(f"{title}\\nNumber of pages: {num_pages}\\n\\n")

    # 4
    for page in pdf_reader.pages:
        text = page.extractText()
        output_file.write(text)

Давайте разберемся что делает код из примера выше:

  1. Сначала вы создаете новый экземпляр класса PdfFileReader и передаете ссылку на него в переменную pdf_reader. Затем создаем новый объект типа Path, который указывает путь к новому текстовому файлу Pride_and_Prejudice.txt на вашем компьютере и присваиваем его переменной output_file_path.
  2. Далее открываем файл по пути содержащемся в output_file_path в режиме записи и присваиваем полученный файлоподобный объект, возвращенный методом .open() переменной output_file. Использование инструкции with гарантирует, что файл будет корректно закрыт и следовательно освободит ресурсы системы, когда код в блоке with закончит свою работу.
  3. Внутри блока with получаем содержимое заголовка нашего PDF файла и количество страниц в документе, а затем записываем полученные данные в текстовый файл, используя метод output_file.write().
  4. И наконец, в цикле for осуществляем перебор всех страниц нашего документа. На каждом шаге в цикле переменной page присваивается объект типа PageObject, которые как мы знаем, является представлением отдельной страницы в документе, а также предоставляет доступ к ее содержимому. Текст с каждой страницы извлекается с помощью метода page.extractText() и записывается в текстовый файл output_file.

Когда вы сохраните этот код и запустите программу, она создаст в вашем рабочем каталоге новый файл Pride_and_Prejudice.txt со всем текстовым содержимым документа Pride_and_Prejudice.pdf. Проверьте это!

Извлекаем отдельные страниц из PDF документа

В предыдущем разделе мы разобрались, как извлечь текст из PDF файла и сохранить его в текстовый файл .txt. Теперь мы узнаем, как извлечь из PDF документа страницу или диапазон страниц, а затем сохранить их в новом PDF файле.

Для создания нового PDF файла вы можете использовать класс PdfFileWriter. Давайте ознакомимся с его возможностями и рассмотрим, что необходимо сделать для создания PDF файла с помощью пакета PyPDF2.

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

Класс PdfFileWriter создает новые PDF файлы. В интерактивном окне IDLE импортируем класс PdfFileWriter, создадим новый объект экземпляра класса и передадим его в переменную pdf_writer:

from PyPDF2 import PdfFileWriter
pdf_writer = PdfFileWriter()

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

Добавим пустую страницу в объект pdf_writer:

page = pdf_writer.addBlankPage(width=72, height=72)

Параметры widthи height являются обязательными и определяют размеры страницы в единицах, называемых point пункт (точка). Один пункта равна 1/72 дюйма, поэтому, приведенный выше пример кода, добавит пустую страницу в объект pdf_writer размером один на один дюйм. Метод addBlankPage() возвращает новый объект PageObject, представляющий пустую страницу, которую вы только что добавили в объект PDF документа PdfFileWriter:

>>> type(page)
<class 'PyPDF2.pdf.PageObject'>

В этом примере, мы передали ссылку на экземпляр объекта PageObject, возвращаемого методом addBlankPage() переменной page, однако на практике вам обычно не нужно это делать. То есть вы просто вызываете метод addBlankPage() без присвоения возвращаемого значения отдельной переменной:

pdf_writer.addBlankPage(width=72, height=72)

Чтобы записать содержимое объекта pdf_writer в PDF файл, передайте объект файла, создающийся при его открытии в бинарном режиме записи, в метод pdf_writer.write():

from pathlib import Path
with Path("blank.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Этот код создаст новый файл с именем blank.pdf в текущем рабочем каталоге. Далее если вы откроете этот файл с помощью программы для чтения PDF документов, например Adobe Acrobat, то увидите документ с одной пустой страницей размером в один дюйм.

Техническая деталь: обратите внимание, что вы сохраняете PDF файл, передавая файлоподобный объект в метод write() экземпляра класса PdfFileWriter, а не в метод write() файлоподобного объекта output_file.

В частности, следующий код не будет работать:

with Path("blank.pdf").open(mode="wb") as output_file:
    output_file.write(pdf_writer)

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

Объекты типа PdfFileWriter могут записывать данные в новые файлы PDF, но они не могут создавать новый контент, кроме пустых страниц.

Это может показаться проблемой, но в большинстве ситуаций вам не нужно создавать новый контент. Чаще вы будете работать со страницами, извлекаемыми из PDF файлов, которые предварительно открыли с помощью объекта класса PdfFileReader.

В приведенном выше примере мы проделали три шага для создания нового PDF файла с использованием PyPDF2:

  1. Создали экземпляр класса PdfFileWriter.
  2. Добавили одну или несколько страниц в новый объект PdfFileWriter.
  3. Записали данные в файл, используя метод PdfFileWriter.write().

Вы будете видеть этот паттерн кода снова и снова, пока мы будем изучать способы добавления страниц и содержимого в экземпляр класса PdfFileWriter.

Извлекаем страницу из PDF документа

Давайте вернемся к нашему PDF файлу «*Гордость и предубеждение», с которым мы уже имели дело в предыдущем разделе. Откроем этот файл и извлечем из него первую страницу, а затем создадим новый PDF файл, содержащий эту извлеченную страницу.

Откройте интерактивное окно IDLE и импортируйте классы PdfFileReader и PdfFileWriter из пакета PyPDF2, а также класс Path из pathlib:

from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWriter

Теперь откроем файл Pride_and_Prejudice.pdf с помощью нового экземпляра класса PdfFileReader:

# Измените путь к файлу для корректной работы на вашем компьютере
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)
input_pdf = PdfFileReader(str(pdf_path))

Передадим индекс первой страницы в метод getPage(), в нашем случае это 0, для того, чтобы получить представление первой страницы в виде объекта типа PageObject:

first_page = input_pdf.getPage(0)

Теперь создадим новый экземпляр класса PdfFileWriter и добавим к нему, используя метод addPage(), объект первой страницы first_page:

pdf_writer = PdfFileWriter()
pdf_writer.addPage(first_page)

Метод addPage(), так же , как и метод addBlankPage(), добавляет страницу, к имеющемуся в объекте pdf_writer, набору страниц. Разница в использовании этих методов состоит в том, что метод addPage() требует передачи ему в качестве аргумента объекта типа PageObject.

Теперь запишем страницы из объекта pdf_writer в новый файл:

with Path("first_page.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь у нас есть новый PDF файл first_page.pdf, сохраненный в текущем рабочем каталоге, который содержит только титульную страницу из файла Pride_and_Prejudice.pdf.

Извлекаем нескольких страниц из PDF документа

Давайте извлечем из файла Pride_and_Prejudice.pdf первую главу и сохраним ее в новом PDF файле.

Если мы откроем файл Pride_and_Prejudice.pdf программой для просмотра PDF, то увидим, что первая глава находится на второй, третьей и четвертой страницах. И поскольку страницы индексируются, начиная с 0, то нам необходимо извлечь страницы с индексами 1, 2 и 3.

Теперь, как в предыдущем разделе, импортируем нужные нам классы и откроем исходный PDF файл для чтения:

from PyPDF2 import PdfFileReader, PdfFileWriter
from pathlib import Path
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)
input_pdf = PdfFileReader(str(pdf_path))

И так, наша задача состоит в том, чтобы извлечь из файла страницы с индексами 1, 2 и 3, добавить их в новый объект класса PdfFileWriter, а затем записать его содержимое в новый PDF файл.

Один из возможных способов сделать это в цикле перебирать диапазон чисел от 1 до 3, извлекая новую страницу, с соответствующим индексом, на каждом шаге, а затем добавлять ее в объект PdfFileWriter:

pdf_writer = PdfFileWriter()
for n in range(1, 4):
    page = input_pdf.getPage(n)
    pdf_writer.addPage(page)

В каждой итерации цикла осуществляется перебор страниц по индексам 1, 2 и 3 с использованием функции генератора range(1, 4). Отметим, что диапазон значений возвращаемых этой функцией не включает в себя его правую границу. На каждом шаге цикла вызывается метод getPage(), он извлекает по текущему значению индекса n очередную страницу, которая в свою очередь с использованием метода addPage() добавляется в объект pdf_writer.

Теперь в объект pdf_writer помещено три страницы. Количество страниц вы можете получить используя метод getNumPages():

>>> pdf_writer.getNumPages()
3

И наконец, теперь мы можем записать извлеченные страницы в новый PDF файл:

with Path("chapter1.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

В текущем рабочем каталоге вы можете открыть новый файл chapter1.pdf, который содержит первую главу романа «Гордость и предубеждение» .

Существует еще один способ извлечь несколько страниц из PDF файла: воспользоваться тем, что содержимое атрибута PdfFileReader.pages поддерживает синтаксис нотации срезов. Давайте повторим предыдущий пример, используя значение атрибута pages вместо цикла с использованием функции-генератора range.

Как и в предыдущем примере с инициализации нового объекта PdfFileWriter:

pdf_writer = PdfFileWriter()

Теперь вместо цикла, используя синтаксис срезов, выберем из pages страницы с нужными индексами (от 1 до 3):

for page in input_pdf.pages[1:4]:
   pdf_writer.addPage(page)

Помните, что значения среза содержат элементы с первого, указанного в квадратных скобках индекса, и далее (в право), но не включают элемент со вторым, указанным после двоеточия, номером. Таким образом, следующая инструкция pages[1:4] возвращает итератор, содержащий страницы с индексами 1, 2 и 3.

И по аналогии, запишем содержимое объекта pdf_writer в выходной файл:

with Path("chapter1_slice.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь откроем файл chapter1_slice.pdf, который находится в текущем рабочем каталоге и сравним его с файлом chapter1.pdf, который мы создали ранее используя цикл. Они содержат одинаковые страницы!

В некоторых случаях необходимо извлечь все страницы из PDF документа для последующей обработки. Для этого вы можете использовать методы, рассмотренные выше, но PyPDF2 предоставляет более простое решение. Объекты класса PdfFileWriter имеют в своем составе метод appendPagesFromReader(), который можно использовать для добавления всех страниц из PdfFileReader.

Чтобы использовать метод appendPagesFromReader(), передайте ему объект типа PdfFileReader в качестве параметра reader. Например, следующий код копирует каждую страницу из файла «Гордость и предубеждение» в объект PdfFileWriter:

pdf_writer = PdfFileWriter()
pdf_writer.appendPagesFromReader(pdf_reader)

Теперь объект pdf_writer содержит все страницы из pdf_reader.

Объединение и слияние содержимого PDF файлов

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

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

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

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

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

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

Основное различие между ними заключается в том, что класс PdfFileWriterможет добавлять или объединять страницы документов, добавляя их в конец документа, тогда как PdfFileMerger может вставлять или объединять страницы в любом месте PDF документа.

И так создадим свой первый объект экземпляр класса PdfFileMerger. В интерактивном окне IDLE введите следующий код для импорта класса PdfFileMerger и создания нового объекта:

from PyPDF2 import PdfFileMerger
pdf_merger = PdfFileMerger()

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

Существует несколько способов добавить страницы к объекту типа pdf_merger. Какой из них использовать будет зависеть от решаемой задачи:

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

Далее в этом разделе мы рассмотрим оба метода и начнем с append().

Объединяем PDF файлы с методом append()

В папке practice_files/ находится подкаталог, expense_reports, который содержит три отчета о расходах для сотрудника по имени Питер Пайтон.

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

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

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

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

И так у нас есть путь к каталогу expense_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() сортирует список непосредственно, то есть вам не нужно присваивать возвращаемое значение отдельной переменной. Таким образом после вызова list() список expense_reports будет по молчанию отсортирован в алфавитном порядке.

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

Выглядит хорошо!

Теперь вы можете объединить все три PDF файла в один. Для этого мы будем использовать метод PdfFileMerger.append() с одним строковым аргументом, содержащим путь к новому файлу. При вызове метода 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, содержащий путь к соответствующему файлу в каталоге cost_reports/ перед передачей в метод pdf_merger.append() преобразуется в строку с помощью функции str().

И так мы объединили все файлы из каталога cost_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)

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

Слияние содержимого PDF файлов с помощью метода merge()

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

Взгляните на следующий пример. Компания Goggle, Inc. подготовила очередной ежеквартальный отчет, но забыла включить в него оглавление. Питер Пайтон заметил ошибку и быстро создал файл с отсутствующим содержанием отчета. Теперь ему нужно вставить этот PDF файл в нужное место файла с исходным отчетом.

Как файл отчета, так и файл с оглавлением можно найти в подпапке quarterly_report/ папки Practice_files. Отчет находится в файле с именем report.pdf, а оглавление — в toc.pdf.

В интерактивном окне IDLE импортируем класс PdfFileMerger, а также, как всегда, создайте объекты Path, содержащие пути к файлам report.pdf и toc.pdf:

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, то увидите, что первая страница отчета является титульной страницей. Вторая — введением, а остальные содержат различные разделы отчета.

И так, нам необходимо вставить оглавление после титульного листа, перед разделом введения. И поскольку индексы страниц в пакете PyPDF2 начинаются с 0, нужно вставить оглавление после страницы с индексом 0 перед страницей с индексом 1.

Для этого необходимо вызвать метод pdf_merger.merge() передав ему следующие два значения в качестве аргументов:

  1. Целое число 1, обозначающее индекс страницы, в которую следует вставить оглавление.
  2. Строку, содержащую путь к файлу с оглавлением.

Вот как это будет выглядеть на практике:

pdf_merger.merge(1, str(toc_path))

Каждая страница из оглавления вставляется перед страницей с указателем 1. И поскольку оглавление состоит только из одной страницы, то оно вставляется в позицию страницы с указателем 1. Соответственно страница, имеющая в текущий момент индекс 1 будет перемещена в позицию с индексом 2. А страница, которая в находилась в позиции с индексом, 2 смещается в индекс 3 и т.д.

Запишем объединенный документ в выходной файл:

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

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

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

Поворот и обрезка страниц PDF документа

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

Поворачиваем страницы

Начнем с изучения, как поворачивать страницы. Для этого примера мы будем использовать файл ugly.pdf в папке practice_files. Файл ugly.pdf содержит фрагмент книги Андерсена Гадкий утенок , за исключением того, что каждая нечетная страница повернута против часовой стрелки на девяносто градусов.

Давайте это исправим. В новом интерактивном окне IDLE , уже традиционно, начнем с импорта классов PdfFileReader и PdfFileWriter из пакета PyPDF2, а также класса Path из модуля pathlib.

from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWriter

Теперь создайте новый объект Path , который будет содержать путь до файла ugly.pdf:

pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "ugly.pdf"
)

И наконец, создайте новые объекты экземпляров классов PdfFileReader и PdfFileWriter:

pdf_reader = PdfFileReader(str(pdf_path))
pdf_writer = PdfFileWriter()

И снова наша цель использовать объект pdf_writer для создания нового файла, в котором все страницы будут иметь правильную ориентацию (или нужную нам). В нашем файле примера ugly.pdf страницы с четными номерами в PDF уже ориентированы нужным образом, а страницы с нечетными номерами перевернуты против часовой стрелки на девяносто градусов.

Чтобы исправить эту проблему, мы будем использовать метод PageObject.rotateClockwise(). Этот метод принимает целочисленный аргумент в градусах и поворачивает страницу по часовой стрелке на заданной угол. Например, вызов метода rotateClockwise(90) осуществляет поворот страницы по часовой стрелке на девяносто градусов.

Примечание. В дополнение к методу .rotateClockwise() в классе PageObject также реализован метод .rotateCounterClockwise(), который предназначен для вращения страниц против часовой стрелки.

Существует несколько способов поворачивать страницы в PDF документах, но в этом руководстве мы рассмотрим только два способа сделать это. И хотя они оба используют метод rotateClockwise(), разница заключается лишь в способе определения, какие именно страницы будут поворачиваться.

Первый способ заключается в том, чтобы циклически проходить по индексам страниц в документе и проверять соответствует ли текущий номер индекса индексу страницы, которую необходимо повернуть. И если это так, то для поворота страницы будем вызывать метод rotateClockwise(), и затем она в любом случае будет добавлена в объект pdf_writer для последующей записи в файл.

Вот как это выглядит на практике:

for n in range(pdf_reader.getNumPages()):
    page = pdf_reader.getPage(n)
    if n % 2 == 0:
        page.rotateClockwise(90)
    pdf_writer.addPage(page)

Обратите внимание, что в этом примере страница поворачивается, если ее индекс четный. Это может показаться странным, поскольку нечетные страницы в нашем PDF файле это те, которые повернуты неправильно. Однако номера страниц в PDF документе начинаются с 1, а индексы страниц в объекте начинаются с 0. Это означает, что нечетные страницы PDF имеют четные индексы!

Если это заставляет вашу голову задымиться, не волнуйтесь! Даже после многих лет работы с подобными задачами, профессиональные программисты все еще сталкиваются с такого рода казусами!

Примечание. Когда вы запустите этот код на выполнение, то результатом работы цикла for будет множество данных, отображаемых в интерактивном окне IDLE. Это происходит потому, что метод rotateClockwise()каждый раз будет возвращать объект экземпляра класса PageObject.

Теперь, когда вы повернули все страницы в PDF файле, вы можете записать содержимое pdf_writer в новый файл и проверить, что все работает так, как нам нужно:

with Path("ugly_rotated.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь у нас в текущем рабочем каталоге будет находится новый файл ugly_rotated.pdf, в котором все страницы из файла ugly.pdf будут с правильной ориентацией.

Однако существует небольшая проблема с подходом, который мы только что использовали для поворота страниц в файле ugly.pdf. Она заключается в том, что необходимо заранее знать, какие страницы нужно повернуть. Однако, в реальном сценарии непрактично перебирать все страницы файла, отмечая в ручную, какие страницы нужно повернуть.

На самом деле, вы можете определить, какие страницы нужно поворачивать без предварительного заданной логики, так сказать делая это автоматически.

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

pdf_reader = PdfFileReader(str(pdf_path))

И так, мы уже ранее поместили исправленные страницы в старый объект класса PdfFileReader, то есть уже повернули их и объект содержит исправленный набор страниц. Таким образом, создавая новый экземпляр класса, мы начинаем заново.

Объекты класса PageObject содержат данные скомпонованные в виде словаря с различного рода информацией о странице:

>>> pdf_reader.getPage(0)
{'/Contents': [IndirectObject(11, 0), IndirectObject(12, 0),
IndirectObject(13, 0), IndirectObject(14, 0), IndirectObject(15, 0),
IndirectObject(16, 0), IndirectObject(17, 0), IndirectObject(18, 0)],
'/Rotate': -90, '/Resources': {'/ColorSpace': {'/CS1':
IndirectObject(19, 0), '/CS0': IndirectObject(19, 0)}, '/XObject':
{'/Im0': IndirectObject(21, 0)}, '/Font': {'/TT1':
IndirectObject(23, 0), '/TT0': IndirectObject(25, 0)}, '/ExtGState':
{'/GS0': IndirectObject(27, 0)}}, '/CropBox': [0, 0, 612, 792],
'/Parent': IndirectObject(1, 0), '/MediaBox': [0, 0, 612, 792],
'/Type': '/Page', '/StructParents': 0}

Среди всех этих бессмысленно выглядящих данных, наше внимание в четвертой строке привлекает ключ /Rotate со значением -90.

В объекте PageObject вы можете получить доступ к значению ключа /Rotate, используя нотацию с квадратными скобками точно так же, как и для обычного объекта dict в Python:

>>> page = pdf_reader.getPage(0)
>>> page["/Rotate"]
-90

Если же мы посмотрим на значение ключа /Rotate для второй страницы pdf_reader, то увидим, что он имеет значение 0:

>>> page = pdf_reader.getPage(1)
>>> page["/Rotate"]
0

Это значит, что страница с индексом 0 имеет значение поворота -90 градусов. Другими словами, он был до этого повернут против часовой стрелки на девяносто градусов. Страница в индексе 1 имеет значение поворота 0, следовательно она вообще не вращалась.

Если вы поворачиваете первую страницу используя метод rotateClockwise(), то значение /Rotate изменится с -90 на 0:

>>> page = pdf_reader.getPage(0)
>>> page["/Rotate"]
-90
>>> page.rotateClockwise(90)
>>> page["/Rotate"]
0

Теперь, когда мы знаем, как проверить значение ключа /Rotate, рассмотрим, как мы можем использовать это для поворота нужных страниц в файле ugly.pdf.

Первое, что нам нужно сделать, повторно инициализировать новые экземпляры классов pdf_reader и pdf_writer для того, чтобы начать все сначала:

pdf_reader = PdfFileReader(str(pdf_path))
pdf_writer = PdfFileWriter()

Теперь напишем цикл по набору страниц, содержащихся в объекте pdf_reader.pages. В каждой итерации цикла будем проверять значение ключа /Rotate и поворачивать страницу, если оно равно -90:

for page in pdf_reader.pages:
    if page["/Rotate"] == -90:
        page.rotateClockwise(90)
    pdf_writer.addPage(page)

Как видим код этого цикла немного короче, чем в первом предложенном решении, и его работа не зависит от наших знаний о том, номера каких страницы нужно поворачивать. Теперь вы можете использовать данный цикл, чтобы поворачивать страницы в любом PDF документе, даже предварительно не открывая и не просматривая его.

Чтобы завершить наше новое решение этой задачи, запишем содержимое pdf_writer в новый файл:

with Path("ugly_rotated2.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь мы можем открыть файл ugly_rotated2.pdf в текущем рабочем каталоге и сравнить его с ранее созданным файлом ugly_rotated.pdf. Они должны выглядеть одинаково.

Примечание: одно маленькое предупреждение об использовании в нашем решении задачи ключа /Rotate: существование это ключа в словаре свойств страницы не гарантируется в обязательном порядке.

Если ключа /Rotate не существует, то это обычно означает, что страница не была повернута. Однако это тоже не всегда верное предположение.

Если у объекта PageObject нет ключа /Rotate, то при попытке доступа к нему будет возбуждаться исключение типа KeyError. Вы можете перехватить его и обработать с использованием блока try...except.

Значение ключа /Rotate не всегда может быть тем, что вы именно ожидаете. Например, если вы сканируете бумажный документ со страницей, которую при сканировании перевернули на девяносто градусов против часовой стрелки, содержимое PDF будет отображаться повернутым. Однако значение ключа /Rotate будет иметь значение 0.

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

Обрезаем страницы

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

В качестве примера рассмотрим, папку practice_files, которая содержит файл с именем half_and_half.pdf. Этот PDF файл содержит часть сказки «Русалочка» Ганса Христиана Андерсена .

Страницы в этом PDF документе имеют две колонки. Давайте разделим каждую страницу на две отдельные страницы, по одной для каждого столбца колонки.

И как мы уже это ранее делали, сначала импортируем классы PdfFileReaderи PdfFileWriter из пакета PyPDF2и класс Path из модуля pathlib:

from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWrite

Теперь создадим новый объект Path, в котором будет определен путь к файлу half_and_half.pdf:

pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "half_and_half.pdf"
)

Создадим новый новый объект класса PdfFileReader и получим первую страницу PDF документа:

pdf_reader = PdfFileReader(str(pdf_path))
first_page = pdf_reader.getPage(0)

Для того, чтобы обрезать страницу, сначала необходимо узнать немного больше о том, как структурированы страницы нашего документа. Объект first_page как экземпляр класса PageObject содержит атрибут mediaBox, в котором определены параметры прямоугольной области, соответствующей границам страницы.

Воспользуемся интерактивным окном IDLE, и просмотрим содержимое атрибута mediaBox:

>>> first_page.mediaBox
RectangleObject([0, 0, 792, 612])

Атрибут mediaBox содержит объект типа RectangleObject. Этот тип определен в пакете PyPDF2 и представляет собой параметры некоторой прямоугольной области страницы.

Как мы можем видеть, из выведенных в консоли данных, список [0, 0, 792, 612] определяет координаты прямоугольной области. При этом первые два числа — это координаты x и y нижнего левого угла прямоугольника. А третье и четвертое — соответственно ширину и высоту этого прямоугольника. Единицами измерения всех значений являются points (пункты), каждый соответствует 1/72 дюйма.

Таким образом объект RectangleObject([0, 0, 792, 612]) представляет собой прямоугольную область с нижним левым углом в начале координат, шириной 792 точки или 11 дюйма и высотой 612 точек или 8,5 дюйма. Это размеры стандартной страницы документа letter-sized в альбомной ориентации.

В свою очередь объект типа RectangleObject имеет также четыре атрибута, которые возвращают координаты углов прямоугольной области: lowerLeft, lowerRight, upperLeft, и upperRight. Также как и значения ширины и высоты, данные координаты заданы в points (пунктах).

Вы можете использовать значениях эти четырех свойств объекта, чтобы получить координаты любого угла области RectangleObject:

>>> first_page.mediaBox.lowerLeft
(0, 0)
>>> first_page.mediaBox.lowerRight
(792, 0)
>>> first_page.mediaBox.upperLeft
(0, 612)
>>> first_page.mediaBox.upperRight
(792, 612)

Таким образом, каждое свойство содержит кортеж, с координатами соответствующего угла прямоугольной области (страницы документа). Также мы можем получить доступ к отдельным координатам с использованием нотации квадратных скобок, как и в случае с обычным кортежем tuple:

>>> first_page.mediaBox.upperRight[0]
792
>>> first_page.mediaBox.upperRight[1]
612

Изменить координаты прямоугольной области (страницы) можно, присвоив любому из свойств атрибута mediaBox, нового значения в виде кортежа:

>>> first_page.mediaBox.upperLeft = (0, 480)
>>> first_page.mediaBox.upperLeft
(0, 480)

В случае если вы, например, изменяете значения атрибутов upperLeft или upperRight, то остальные координаты области автоматически подстраивается так, чтобы сохранить ее прямоугольную форму:

>>> first_page.mediaBox.upperRight
(792, 480)

Поэтому если вы измените значения координат страницы в объекте RectangleObject, а вернее значения свойств атрибута mediaBox, вы фактически обрезаете страницу. Теперь объект first_page содержит в объекте RectangleObject информацию о новых координатах (параметрах) страницы.

Давайте поместим обрезанную страницу в новый PDF файл:

pdf_writer = PdfFileWriter()
pdf_writer.addPage(first_page)
with Path("cropped_page.pdf").open(mode="wb") as output_file:
	pdf_writer.write(output_file)

Теперь если вы откроете файл cropped_page.pdf, находящийся в рабочем каталоге, то увидите, что верхняя часть страницы документа была удалена.

Как же обрезать страницу так, чтобы был виден только текст в левой части страницы? Правильно, нам нужно будет уменьшить горизонтальные размеры страницы вдвое. Вы можете добиться этого, изменив значение координаты в свойстве upperRight объекта mediaBox. Посмотрим, как это работает.

Во-первых, нам нужно создать новые объекты PdfFileReader и PdfFileWriter, поскольку мы только что изменили первую страницу в объекте pdf_reader, а затем добавили ее в pdf_writer:

pdf_reader = PdfFileReader(str(pdf_path))
pdf_writer = PdfFileWriter()

Теперь получим первую страницу нашего документа:

first_page = pdf_reader.getPage(0)

В этот раз давайте поработаем с копией первой страницы, чтобы только что извлеченная страница осталась нетронутой. Вы можете сделать это, импортируя модуль copy из стандартной библиотеки Python, используя его функцию deepcopy(), чтобы гарантированно сделать точную копию первой страницы:

import copy
left_side = copy.deepcopy(first_page)

Теперь мы можем изменить значение свойства left_side, не изменяя свойств исходного объекта first_page. Таким образом, мы можем позже использовать first_page для извлечения текстового содержимого из правой части страницы.

Теперь займемся математикой. Как мы уже выяснили, что нам нужно переместить координаты верхнего правого угла, изменяя соответствующие значения в mediaBox, в верх и центр страницы. Для этого создадим новый кортеж tuple со значением, равным половине исходного значения соответствующей координаты верхнего правого угла, а затем передадим его в свойство upperRight.

Для этого сначала получим текущие координаты правого верхнего угла из объекта mediaBox.

current_coords = left_side.mediaBox.upperRight

Теперь создадим новый кортеж tuple с координатами угла, при этом его первая координата будет равна половине текущего значения координаты, а вторая — такая же, как у оригинала:

new_coords = (current_coords[0] / 2, current_coords[1])

И наконец, передадим значения новых координаты угла в свойство upperRight:

left_side.mediaBox.upperRight = new_coords

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

Для этого сначала создадим новую копию объекта, содержащего первую страницу first_page:

right_side = copy.deepcopy(first_page)

Переместим значения координат из свойства upperLeft (левый верхний угол) в свойство upperRight (верхний правый угол):

right_side.mediaBox.upperLeft = new_coords

Этот код устанавливает для верхнего левого угла страницы те же координаты, которые мы поместили в свойство верхнего правого угла при извлечении левой части страницы. Итак, объект right_side.mediaBox теперь представляет собой прямоугольник, левый верхний угол которого находится в центре верхней части страницы, а правый верхний угол — в правом верхнем углу страницы.

Наконец, добавим объекты страниц left_side и right_side в pdf_writer и запишем их в новый файл:

pdf_writer.addPage(left_side)
pdf_writer.addPage(right_side)
with Path("cropped_pages.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

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

Шифрование и дешифрование PDF файлов

Иногда PDF файлы бывают защищены паролем. С пакетом PyPDF2 вы можете работать с зашифрованными PDF файлами, а также добавлять защиту паролем к существующим файлам.

Шифрование PDF

Вы можете добавить защиту паролем к PDF файлу, используя метод encrypt() соответствующего экземпляра класса PdfFileWriter при записи файла. У этого метода есть два основных именованных строковых параметра:

  1. user_pwd устанавливает пароль пользователя. Это позволяет открывать и читать PDF файл.
  2. owner_pwd устанавливает пароль владельца. Это позволяет работать с PDF без ограничений и в том числе редактировать его.

Давайте воспользуемся методом encrypt() , чтобы добавить пароль к существующему PDF файлу. Для этого сначала откроем файл newsletter.pdf в каталоге Practice_files/:

from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWriter
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "newsletter.pdf"
)
pdf_reader = PdfFileReader(str(pdf_path))

Теперь создайте новый экземпляр класса PdfFileWriter и добавим в него страницы из объекта pdf_reader:

pdf_writer.encrypt(user_pwd="SuperSecret")

Если вы устанавливаете только user_pwd, то аргумент owner_pwd по умолчанию будет равен той же передаваемой строке. Таким образом, эта строка кода устанавливает пароли как пользователя, так и владельца.

Наконец, запишем зашифрованный документ в выходной файл с именем newsletter_protected.pdf в рабочий каталог:

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

Теперь если вы откроете этот PDF файл с помощью программы для чтения PDF, то вам будет предложено ввести пароль. Введите «SuperSecret», чтобы открыть PDF файл.

Если вам нужно установить отдельный пароль для владельца файла, то передайте строку в параметр owner_pwd:

user_pwd = "SuperSecret"
owner_pwd = "ReallySuperSecret"
pdf_writer.encrypt(user_pwd=user_pwd, owner_pwd=owner_pwd)

В этом примере пароль пользователя — «SuperSecret», а пароль владельца — «ReallySuperSecret».

Теперь если вы зашифруете PDF файл паролем, а затем попытаетесь его открыть, то должны указать пароль, прежде чем сможете просматривать и редактировать его содержимое. Эта защита распространяется также на чтение и редактирование PDF в программах написанных как на Python, так и на других языках программирования.

Дешифрование PDF

Чтобы расшифровать ранее защищенный паролем PDF файл, используйте метод decrypt() экземпляра класса PdfFileReader.

Метод decrypt() имеет единственный параметр password, который используется как пароль для дешифрования содержимого PDF документа. Права, которые предоставляются вам при открытии PDF файла, зависят от аргумента, который вы передали в параметр password.

Откроем зашифрованный файл newsletter_protected.pdf, который мы создали в предыдущем разделе, и воспользуемся возможностями пакета PyPDF2 для его расшифровки.

Сначала создадим новый экземпляр класса PdfFileReader и путь к защищенному PDF файлу:

from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWriter
pdf_path = Path.home() / "newsletter_protected.pdf"
pdf_reader = PdfFileReader(str(pdf_path))

Прежде чем расшифровать файл, проверим, что произойдет, если мы попытаемся получить первую страницу документа:

>>> pdf_reader.getPage(0)
Traceback (most recent call last):
  File "/Users/damos/github/realpython/python-basics-exercises/venv/
  lib/python38-32/site-packages/PyPDF2/pdf.py", line 1617, in getObject
    raise utils.PdfReadError("file has not been decrypted")
PyPDF2.utils.PdfReadError: file has not been decrypted

Было возбуждено исключение типа PdfReadError, информирующее нас о том, что PDF файл не был расшифрован.

Давайте расшифруем его:

>>> pdf_reader.decrypt(password="SuperSecret")
1

Метод decrypt() возвращает целое число, представляющее информацию об успешности процесса дешифрации:

  • 0 — означает, что пароль неверный.
  • 1 — означает, что был найден пароль пользователя.
  • 2 — означает, что был найден пароль владельца файла.

После того, как вы расшифровали файл, можно получить доступ к содержимому PDF документа:

>>> pdf_reader.getPage(0)
{'/Contents': IndirectObject(7, 0), '/CropBox': [0, 0, 612, 792],
'/MediaBox': [0, 0, 612, 792], '/Parent': IndirectObject(1, 0),
'/Resources': IndirectObject(8, 0), '/Rotate': 0, '/Type': '/Page'}

Теперь вы можете извлекать текстовое содержимое, обрезать, а также поворачивать страницы документа по своему усмотрению!

Создаем PDF файл

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

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. И если у вас открыто окно IDLE, вам необходимо перезапустить его, прежде чем вы сможете использовать пакет reportlab после его установки.

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

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

Откройте новое интерактивное окно IDLE и введите следующие инструкции кода, чтобы импортировать класс Canvas:

canvas = Canvas("hello.pdf")

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

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

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

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

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

Для того, чтобы сохранить созданное нами содержимое документа в файл, используется метод save():

canvas.save()

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

  • В отношении только что созданного PDF файла следует отметить некоторые особенности: Размер страницы по умолчанию A4, что не совпадает со стандартным размером страницы Letter в США.
  • По умолчанию используется шрифт Helvetica с размером шрифта 12 пунктов.

Но пока останавливаться на настройках отображения содержимого мы не будем, а поговорим о них далее.

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

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

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

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

Размер (612, 792) соответствует бумаге формата Letter, потому что 8,5 умножить на 72 равно 612, а 11 умножить на 72792.

Если вычисления преобразований пунктов в дюймы или сантиметры для вас утомительны, вы можете использовать модуль reportlab.lib.units, который поможет вам в этом. Модуль units содержит несколько вспомогательных объектов, таких как inch (дюйм) и cm (см), которые упрощают процесс прямого и обратного преобразования величин размеров и расстояний.

Импортируем объекты inch и cm из модуля reportlab.lib.units:

from reportlab.lib.units import inch, cm

Проверим содержимое каждого объекта, чтобы увидеть, что они собой представляют:

>>> cm
28.346456692913385
>>> inch
72.0

И так, cm, и inch представляют собой числовые значения с плавающей запятой. Они содержат количество пунктов points, содержащихся в соответствующей единице измерения. inch равен 72,0 пункта, а cm — 28,346456692913385 пунктов.

Использование объектов единиц измерения при преобразовании, заключается в умножении значения переменной (количества единиц) на объект с соответствующим названием единицы измерения. Например, вот так с помощью объекта inch устанавливается размер страницы 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 содержит множество других стандартных размеров страниц. Вот несколько примеров с указанием их размеров:

Тип страницыРазмеры
A4210 mm x 297 mm
LETTER8.5 in x 11 in
LEGAL8.5 in x 14 in
TABLOID11 in x 17 in

В дополнение к этому, модуль содержит определения для всех стандартных форматов бумаги стандарта ISO 216.

Настраиваем свойства шрифта

При создании новых PDF документов, записи текста в объект Canvas вы можете изменить вид (семейство) шрифта, его размер, а также его цвет.

Используйте метод setFont(), чтобы изменить шрифт, его размер и другие характеристики.

Создадим новый экземпляр класса Canvas, которому передадим в качестве значений аргументов имя файла font-example.pdf и размер страницы документа letter:

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

Для примера установим шрифт 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"

Каждый шрифт имеет жирный и курсивный варианты. Ниже приведен список всех вариантов шрифтов, доступных в reportlab:

  • "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)

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

# Сохраним текст в PDF файл
canvas.save()

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

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

Руководство пользователя ReportLab содержит множество примеров того, как создавать PDF документы с нуля. Это отличное начало, если вам стало интересно узнать больше о создании PDF файлов с помощью Python.

Заключение: создаем и изменяем PDF файлы в Python

В этом руководстве мы научились создавать и изменять PDF файлы с помощью пакетов PyPDF2 и reportlab.

Вы узнали как с помощью пакета PyPDF2:

  • читать PDF файлы и извлекать из них текстовое содержимое с помощью класса PdfFileReader;
  • создавать новые PDF файлы с помощью класса PdfFileWriter;
  • объединять и сливать в нужном нам порядке содержимое PDF файлов с помощью класса PdfFileMerger;
  • поворачивать и обрезать страницы PDF документа.

Вы научились создавать новые PDF файлы с помощью пакета reportlab:

  • Использовать класс Canvas для создания новых PDF файлов.
  • Записывать текст на холсте Canvas с использованием метода drawString().
  • Устанавливать нужный шрифт, его размер и другие характеристики с помощью метода setFont().
  • Задавать цвет шрифта с помощью метода setFillColor().

reportlab — это мощный инструмент для создания и редактирования PDF файлов, и вы только прикоснулись к его возможностям.

2 комментария

Оставить комментарий