pikepdf позволяет программно объединять и разделять PDF, переставлять и поворачивать страницы, исправлять повреждённую структуру, менять XMP-метаданные, извлекать исходные изображения, добавлять вложения, задавать шифрование и сохранять документ с линейной загрузкой. Работа строится вокруг объектов Pdf, Page, Stream и Job: файл открывается в Python, нужные элементы меняются через понятные коллекции и словари, после чего результат записывается с выбранными параметрами совместимости и сжатия.
Вместо окон и панелей инструментов здесь используется Python-код. Пользователь получает прямой доступ к дереву объектов PDF и одновременно может выполнять типовые операции несколькими строками: открыть документ через Pdf.open(), обратиться к pdf.pages, изменить страницу, метаданные или вложения и вызвать pdf.save(). Такой подход особенно удобен для пакетной обработки, серверных сценариев, тестов и утилит, где одинаковое правило должно применяться к сотням файлов без ручного повторения действий.
Основа рабочего процесса — аккуратное изменение уже существующего PDF без обязательного преобразования страниц в изображения. pikepdf сохраняет векторный текст, шрифты, формы, аннотации и сжатые потоки, пока код явно не удаляет или не заменяет соответствующие объекты. При открытии механизм qpdf проверяет структуру и способен восстановить многие ошибки таблицы перекрёстных ссылок; при сохранении строится согласованный файл, поэтому инструмент полезен не только для перестановки страниц, но и для нормализации документов, которые нестабильно открываются в других программах.
Скачать pikepdf
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- Нет графического интерфейса
- Не извлекает текст
- Не рендерит страницы
Как начинается работа с документом
Для обычного проекта создают виртуальное окружение Python и устанавливают пакет командой python -m pip install pikepdf. Готовые колёса содержат необходимую двоичную часть для распространённых сочетаний Python и операционной системы, поэтому компилятор в стандартном сценарии не нужен. Поддерживается Python 3.10 и новее. Если пакет собирается из исходников, понадобятся инструменты C/C++, заголовки qpdf, zlib и libjpeg; такой путь оправдан главным образом при использовании системной сборки qpdf, нестандартной архитектуры или собственных флагов компиляции.
Импорт обычно ограничивается классами, которые нужны конкретной задаче: from pikepdf import Pdf, Page, Name, Dictionary, Array, Encryption. Открывать файл безопаснее контекстным менеджером. Он гарантирует закрытие дескриптора даже при исключении и делает срок жизни связанных объектов очевидным. Страница, поток или словарь зависят от владельца Pdf; если сохранить ссылку на объект, а сам документ удалить, последующее обращение может завершиться DeletedObjectError.
from pikepdf import Pdf
with Pdf.open('input.pdf') as pdf:
print('Страниц:', len(pdf.pages))
print('Версия PDF:', pdf.pdf_version)
pdf.save('copy.pdf')
Pdf.open() принимает путь, pathlib.Path или позиционируемый двоичный поток. Для защищённого файла передают password=. Если пароль неверен, возникает PasswordError, который стоит перехватывать отдельно от общего PdfError: так приложение может запросить другой пароль, не маскируя повреждение структуры. Для новых документов используется Pdf.new(), после чего можно добавлять пустые страницы или переносить страницы из открытых файлов.
Объекты загружаются по мере обращения, поэтому открытие крупного документа само по себе обычно не означает чтение и распаковку каждого изображения. Это экономит время, но требует держать исходный файл доступным до завершения работы. Нельзя закрыть источник сразу после получения списка его страниц, если страницы ещё используются для копирования. На практике источники держат открытыми внутри одного блока with или переносят страницы в целевой Pdf до закрытия.

Коллекция страниц и базовые операции
pdf.pages ведёт себя как изменяемая последовательность. Индексация начинается с нуля, отрицательные индексы работают как в обычном списке Python, срез возвращает выбранный диапазон. Страницу можно удалить оператором del, вставить методом insert(), добавить через append() или перенести группу методом extend(). Изменения происходят в объекте документа в памяти; исходный файл не меняется, пока код не сохранит результат в нужный путь.
from pikepdf import Pdf
with Pdf.open('report.pdf') as pdf:
del pdf.pages[-1] # убрать последнюю страницу
pdf.pages.insert(0, pdf.pages[2])
pdf.pages[1], pdf.pages[3] = pdf.pages[3], pdf.pages[1]
pdf.save('reordered.pdf')
Повторная вставка одной страницы допустима, но важно понимать связь аннотаций и интерактивных полей. Простое копирование страницы внутри документа может оставить общие косвенные объекты. Когда нужны независимые копии форм и аннотаций, применяют операции, учитывающие поля, либо явно копируют аннотации. Для документов без форм обычные методы коллекции чаще всего достаточны.
Поворот выполняет page.rotate(angle, relative=True), где угол кратен 90. При relative=False задаётся абсолютное значение, при relative=True угол прибавляется к существующему. Свойство page.rotation возвращает эффективный поворот с учётом наследования в дереве страниц. Если сторонняя программа неправильно трактует ключ /Rotate, метод flatten_rotation() переносит преобразование в поток содержимого, корректирует рамки и удаляет декларативный поворот, сохраняя внешний вид.

Размеры и области страницы доступны через mediabox, cropbox, trimbox, bleedbox и artbox. Значения заданы в PDF-единицах, обычно равных 1/72 дюйма. Изменение cropbox не удаляет содержимое за границей, а задаёт область показа и печати. Для необратимого удаления данных за пределами рамки одного изменения прямоугольника недостаточно: содержимое остаётся в потоке и может быть извлечено анализатором.
Пользовательские номера страниц не совпадают с индексами коллекции. Отчёт может иметь римские номера во введении, обычные числа в основной части и префиксы в приложении. Метод page.label() возвращает отображаемую метку, если она задана в документе. API удобно читает такие метки, однако готового высокоуровневого редактора структуры PageLabels нет; для создания сложной нумерации приходится работать с соответствующим словарём PDF вручную.
Разделение PDF на отдельные файлы
Для разделения создают новый Pdf на каждой итерации и добавляют в него одну или несколько страниц исходника. Перенос страницы копирует необходимые ресурсы, поэтому шрифты и изображения, реально используемые страницей, остаются доступны. Метаданные уровня всего документа, закладки и часть именованных назначений автоматически не превращаются в метаданные каждого фрагмента; их следует сформировать отдельно, если они важны.
from pathlib import Path
from pikepdf import Pdf
out_dir = Path('pages')
out_dir.mkdir(exist_ok=True)
with Pdf.open('manual.pdf') as source:
for index, page in enumerate(source.pages, start=1):
with Pdf.new() as part:
part.pages.append(page)
part.save(out_dir / f'page-{index:03d}.pdf')
При массовом разделении полезно вызывать remove_unreferenced_resources(). Некоторые производители PDF назначают один общий словарь ресурсов множеству страниц, и отдельная страница после копирования может получить ссылки на шрифты или изображения, которых её поток не использует. Удаление неиспользуемых записей уменьшает размер фрагментов, но выполнять его следует после всех операций с содержимым и перед сохранением.
Объединение нескольких документов
Для конкатенации создают пустой документ и расширяют его коллекцию страниц. Если исходники объявляют разные минимальные версии PDF, целевому файлу задают не меньшую из них через min_version. Иначе функция, допустимая в одном источнике, может оказаться несовместимой с заявленной версией заголовка результата. Метаданные, закладки и вложения нельзя бездумно сливать: одинаковые имена и идентификаторы требуют решения на уровне бизнес-логики.
from pikepdf import Pdf
files = ['cover.pdf', 'chapter-1.pdf', 'chapter-2.pdf']
with Pdf.new() as result:
required_version = result.pdf_version
sources = []
try:
for filename in files:
src = Pdf.open(filename)
sources.append(src)
required_version = max(required_version, src.pdf_version)
result.pages.extend(src.pages)
result.remove_unreferenced_resources()
result.save('book.pdf', min_version=required_version)
finally:
for src in sources:
src.close()
Для больших подборок порядок файлов лучше задавать явно, а не полагаться на строковую сортировку каталога: имя 10.pdf может оказаться перед 2.pdf. Перед переносом проверяют количество страниц, шифрование и читаемость каждого источника. Ошибочный файл тогда можно записать в журнал и пропустить, не теряя уже обработанную очередь.

Наложения, подложки и водяные знаки
Методы Page.add_overlay() и Page.add_underlay() размещают страницу или Form XObject поверх либо под существующим содержимым. Это основа для водяных знаков, фирменных бланков, штампов, номеров, миниатюр и компоновки нескольких страниц на одном листе. Объект помещается в указанный Rectangle; при стандартных параметрах сохраняется пропорция, разрешается уменьшение и увеличение до доступной области.
Наложение добавляется после прежнего потока и способно закрыть текст или изображения. Подложка записывается перед основным содержимым и может оказаться невидимой под непрозрачным фоном. Поэтому выбор метода зависит не от названия задачи, а от строения страницы. Для полупрозрачной отметки поверх скана используют overlay, а готовый бланк под текстовым слоем — underlay.
from pikepdf import Pdf, Rectangle
with Pdf.open('document.pdf') as pdf, Pdf.open('stamp.pdf') as stamp:
mark = stamp.pages[0]
for page in pdf.pages:
target = Rectangle(36, 36, 180, 90)
page.add_overlay(mark, target, shrink=True, expand=False)
pdf.save('stamped.pdf')
Параметр push_stack=True защищает добавленное содержимое от незакрытых графических состояний исходного потока. Он оборачивает прежний поток операторами сохранения и восстановления состояния, но чрезмерное число вложений увеличивает глубину графического стека. Если код добавляет много слоёв, разумнее сначала собрать их в один Form XObject или периодически нормализовать структуру, чем создавать сотни независимых обёрток.
Метод as_form_xobject() превращает страницу в повторно используемый объект. Он удобен для N-up: на пустой странице создают четыре прямоугольника и помещают в них четыре исходные страницы. Преобразования /Rotate и /UserUnit могут быть учтены автоматически. При сборке N-up внутри того же документа меньше риск получить внешние ссылки на объекты другого владельца.
Для точного размещения доступны матрицы. get_matrix_for_form_xobject_placement() вычисляет преобразование, которое вписывает Form XObject в прямоугольник, а Matrix позволяет дополнительно сдвигать, масштабировать и поворачивать объект. Такой уровень нужен, когда отметка должна располагаться относительно печатного поля, а не просто по центру.
Метаданные XMP и DocumentInfo
PDF хранит современные XMP-метаданные и старый словарь DocumentInfo. Через open_metadata() pikepdf синхронизирует распространённые поля между этими представлениями. Редактирование выполняют внутри with: изменения фиксируются при нормальном выходе из блока и отменяются, если внутри возникло исключение. После этого документ всё равно нужно сохранить.
from pikepdf import Pdf
with Pdf.open('report.pdf') as pdf:
with pdf.open_metadata() as meta:
meta['dc:title'] = 'Отчёт отдела качества'
meta['dc:creator'] = ['Иван Петров', 'Анна Соколова']
meta['dc:description'] = 'Проверка комплекта технической документации'
meta['dc:subject'] = ['PDF', 'контроль качества']
pdf.save('report-metadata.pdf')
Для чтения контекстный менеджер не обязателен, но для записи он предотвращает частично применённые изменения. Если XMP-блока нет, интерфейс создаёт пустой контейнер. Поля со сложными структурами можно читать, однако модификация ориентирована прежде всего на скаляры и типовые массивы. Для редких XMP-схем с квалификаторами и вложенными структурами понадобится специализированный XMP-инструмент или низкоуровневая работа с XML.
При переносе метаданных между документами нельзя копировать весь XMP-поток. В нём могут находиться идентификаторы экземпляра, даты, имя производителя и заявления о соответствии PDF/A, PDF/X либо PDF/UA. После объединения или перестройки такие утверждения часто становятся ложными. Безопаснее сформировать белый список описательных полей: название, авторов, описание и тему; технические идентификаторы целевой файл создаст заново.
Свойство meta.pdfa_status показывает, что документ заявляет о соответствии PDF/A, но не валидирует каждое требование стандарта. Для юридически значимой проверки используют специализированный валидатор. При сохранении параметр preserve_pdfa помогает не нарушать ограничения формата действиями самого механизма записи, однако содержательная корректность изменений остаётся ответственностью автора кода.
Чтобы убрать все метаданные, удаляют отдельно pdf.Root.Metadata и pdf.docinfo. Открытие open_metadata() для этой цели нежелательно, потому что интерфейс может создать XMP-контейнер. При очистке следует решить, нужно ли оставлять даты, авторство, пользовательские схемы и служебные поля; безусловное удаление удобно для анонимизации, но может ухудшить поиск и архивное описание.
Шифрование, пароли и ограничения доступа
Защищённый PDF открывают с паролем, а при сохранении передают объект Encryption. Можно задать пользовательский и владельческий пароли, выбрать алгоритм и набор разрешений. Если параметр encryption равен False или не указан для открытого защищённого файла, сохранённая копия будет без прежнего шифрования. Значение True просит перенести параметры исходной защиты.
from pikepdf import Pdf, Encryption, Permissions
permissions = Permissions(
extract=False,
modify_annotation=False,
modify_other=False,
print_lowres=True,
print_highres=False,
)
with Pdf.open('contract.pdf') as pdf:
pdf.save(
'contract-protected.pdf',
encryption=Encryption(
owner='strong-owner-password',
user='reader-password',
allow=permissions,
),
)
Разрешения PDF не являются системой обязательного контроля доступа. Их соблюдает программа просмотра, тогда как инструменты обработки могут прочитать объекты после успешного открытия. Поэтому запрет печати или копирования нельзя считать защитой от мотивированного пользователя. Пароль пользователя ограничивает открытие, но статический файл допускает неограниченное число попыток подбора. Для важных данных нужны сильные пароли, защищённый канал передачи и организационные меры.
Для совместимости пароли лучше составлять из ASCII-символов. pikepdf кодирует строки как UTF-8, но старые просмотрщики могут интерпретировать не-ASCII иначе. После создания защищённого файла полезно проверить его в тех программах, которыми будут пользоваться получатели. Свойства user_password_matched и owner_password_matched помогают приложению понять, каким уровнем доступа открыт документ.
Цифровые подписи pikepdf не создаёт и не проверяет как полноценный подписывающий модуль. Любое сохранение подписанного PDF обычно меняет байтовое представление и может сделать прежнюю подпись недействительной, поскольку запись выполняется заново, а не как инкрементальное обновление. Если подпись нужно сохранить, перед изменением следует изучить допустимый диапазон подписанных байтов и использовать инструмент, рассчитанный на подписи.

Извлечение и анализ изображений
Page.get_images() перечисляет изображения, которые использует страница, включая вложенные Form XObject при рекурсивном режиме. Это важнее старого свойства page.images, которое видит только ресурсы верхнего уровня и может сообщить пустой результат для визуально насыщенной страницы. Один видимый рисунок иногда составлен из нескольких полос, масок и слоёв, а иногда является векторной графикой и вообще не имеет растрового XObject.
Класс PdfImage нормализует доступ к ширине, высоте, цветовому пространству, числу бит на компонент и фильтрам. Метод extract_to() старается сохранить исходное кодирование: JPEG и некоторые другие форматы извлекаются без повторного сжатия. Метод as_pil_image() декодирует изображение в объект Pillow, применяет массив /Decode и по умолчанию объединяет маску прозрачности, чтобы результат соответствовал отображению в просмотрщике.
from pathlib import Path
from pikepdf import Pdf, PdfImage
out = Path('images')
out.mkdir(exist_ok=True)
with Pdf.open('catalog.pdf') as pdf:
for page_no, page in enumerate(pdf.pages, start=1):
for name, image_object in page.get_images().items():
image = PdfImage(image_object)
prefix = out / f'p{page_no:03d}-{str(name).strip("/")}'
image.extract_to(fileprefix=str(prefix))
Извлечение может завершиться UnsupportedImageTypeError, InvalidPdfImageError, DependencyError или ошибкой декодирования. JBIG2, CCITT, JPEG 2000, нестандартные цветовые пространства и высокоточная печатная информация требуют отдельной обработки. В пакетном сценарии исключение фиксируют для конкретного объекта, а остальные изображения продолжают извлекать; иначе один редкий поток остановит весь документ.
Защита Pillow от декомпрессионных бомб действует и при преобразовании через as_pil_image(). Огромное число пикселей вызывает предупреждение или DecompressionBombError. Не следует отключать лимит глобально для непроверенных документов. Безопаснее оценить размеры width × height, установить собственный предел и извлекать исходный поток без полной распаковки, когда это возможно.
Удаление изображения требует изменить поток содержимого или заменить ресурс, а не только удалить запись из словаря. Если оператор Do продолжает ссылаться на отсутствующий XObject, страница становится некорректной. Обратная ситуация тоже встречается: ресурс остаётся в словаре, но больше не вызывается. После редактирования операторов помогает remove_unreferenced_resources().
Вставка произвольного нового растрового файла не является основной высокоуровневой функцией PdfImage. Для водяного знака проще подготовить одностраничный PDF и наложить его. Для точной замены изображения можно создать поток с корректным словарём XObject, фильтрами, цветовым пространством и маской, но это уже работа со спецификацией PDF, где ошибка в одном ключе проявится только в части просмотрщиков.
Вложения и файлы внутри PDF
Коллекция pdf.attachments позволяет добавлять, читать, переименовывать и удалять вложенные файлы. AttachedFileSpec.from_filepath() считывает данные из пути, а AttachedFileSpec(pdf, bytes, mime_type=...) создаёт вложение из памяти. Данные копируются в объект при создании, поэтому исходный файл можно закрыть до сохранения PDF.
from pathlib import Path
from pikepdf import Pdf, AttachedFileSpec
with Pdf.open('invoice.pdf') as pdf:
spec = AttachedFileSpec.from_filepath(pdf, Path('invoice-data.xml'))
pdf.attachments['invoice-data.xml'] = spec
pdf.save('invoice-with-data.pdf')
Если основной документ зашифрован, вложения шифруются теми же параметрами. Просмотрщики обычно сортируют имена по алфавиту, поэтому для заданного порядка используют числовые префиксы. В описание можно записать назначение файла, а поле relationship обозначает связь вроде Source, Data или Supplement. Эти сведения особенно полезны в архивных и машиночитаемых документах.
Вложение в коллекции и аннотация FileAttachment — разные механизмы. Аннотация показывает на странице значок, который открывает файл; обычное вложение отображается в панели просмотрщика. Если один объект добавить обоими способами, пользователь увидит две сущности. Для автоматической выдачи сопутствующих данных обычно достаточно attachments, а значок применяют только тогда, когда место вложения на странице несёт смысл.
Удаляя подозрительные вложения, недостаточно очистить только pdf.attachments: файл может быть достижим через аннотацию, действие или другую структуру. Для санитарной обработки проверяют все точки доступа, а затем убеждаются, что удалённые объекты не достижимы от корня. При сохранении pikepdf записывает только достижимые объекты, поэтому действительно осиротевшие потоки не попадут в результат.
Закладки, назначения и начальный вид
Закладки представлены outline-деревом. Контекст pdf.open_outline() даёт корневой список, в который добавляют OutlineItem. Номер страницы задаётся с нуля. Дочерние пункты хранятся в children, поэтому можно построить иерархию глав и подразделов. Изменение текста существующего пункта не меняет его назначение, если код отдельно не присваивает новое.
from pikepdf import Pdf, OutlineItem
with Pdf.open('book.pdf') as pdf:
with pdf.open_outline() as outline:
chapter = OutlineItem('Глава 1', 0)
chapter.children.append(OutlineItem('1.1 Подготовка', 2))
chapter.children.append(OutlineItem('1.2 Проверка', 5))
outline.root.append(chapter)
outline.root.append(OutlineItem('Приложение', 18))
pdf.save('book-with-bookmarks.pdf')
Назначение может указывать не только страницу, но и способ показа: Fit, FitH, FitB, координаты и масштаб. В существующем PDF пункт иногда содержит действие вместо прямого Dest. При редактировании pikepdf сохраняет исходное действие, пока код не задаст назначение. Некорректное дерево вызывает OutlineStructureError; в такой ситуации безопаснее сначала прочитать и проверить узлы, а не сразу перезаписывать всё дерево.
Свойства Root.PageLayout и Root.PageMode задают рекомендованный начальный вид: одну страницу, две колонки, полноэкранный режим, панель миниатюр, закладок или вложений. Просмотрщик вправе игнорировать эти подсказки и применять предпочтения пользователя. Поэтому ими нельзя заменять навигацию или обязательное предупреждение на первой странице.
При объединении простое pages.extend() не переносит закладки как готовое объединённое дерево. Практичный алгоритм заранее считает начальный индекс каждого источника, создаёт новый пункт с названием файла или раздела и добавляет смещённые дочерние пункты, если они нужны. Именованные назначения могут конфликтовать; их следует переименовать или использовать form-aware методы добавления страниц, которые сообщают о переименованиях.
Формы, аннотации и копирование между документами
Страница может содержать виджеты формы, а поля хранятся также в общем AcroForm. Поэтому перенос страницы — не всегда перенос всей формы. Простое добавление страницы часто сохраняет визуальный вид, но поле может потерять родителя, получить конфликт имени или остаться общей ссылкой с другой копией. Для документов с формами используют add_pages_from() и учитывают возвращаемый PageCopyResult.
Результат сообщает число добавленных страниц, переименованные поля и назначения, частично скопированные поля и другие конфликты. Это позволяет вывести предупреждение, а не молча выпустить документ с двумя полями одинакового имени. Если интерактивность не нужна, формы можно удалить или сплющить специализированным средством до объединения; pikepdf не должен имитировать визуальный рендеринг виджетов без готовых appearance streams.
Page.copy_annotations() переносит аннотации с другой страницы и применяет матрицу к их прямоугольникам. Метод также учитывает form widgets и добавляет поля в AcroForm целевого документа. Он полезен, когда содержимое страницы размещается в другом масштабе: одинаковая матрица должна применяться и к аннотациям, иначе кликабельная область останется на старом месте.
Аннотация может содержать ссылку, JavaScript, запуск файла, отправку формы или вложение. При обработке входящих документов следует считать действия активным содержимым. Функции санитарной очистки умеют нейтрализовать сценарии и внешний доступ, сохраняя видимую рамку ссылки. Это безопаснее, чем удалять все аннотации, среди которых могут быть полезные комментарии и поля.
Низкоуровневая модель объектов PDF
PDF состоит из словарей, массивов, имён, строк, чисел, логических значений, потоков и косвенных ссылок. pikepdf отображает эти сущности на Dictionary, Array, Name, String, Stream и общий Object. Доступ через атрибуты удобен для имён вроде page.Resources, а индексная форма page['/Resources'] подчёркивает точное имя ключа.
from pikepdf import Pdf, Name
with Pdf.open('input.pdf') as pdf:
page = pdf.pages[0]
print(page.mediabox)
resources = page.Resources
if Name.XObject in resources:
for resource_name, obj in resources.XObject.items():
print(resource_name, obj.get('/Subtype'))
Имя PDF обязательно начинается со слеша. Name('/Page') и предопределённое Name.Page эквивалентны. Строка Python и PDF Name не взаимозаменяемы: значение '/Page' как обычная строка создаст строковый объект, а не имя. Ошибка типа может не проявиться до открытия результата строгим валидатором.
Косвенный объект принадлежит одному Pdf. Если вставить сложный объект в другой документ напрямую, возникает ForeignObjectError. Для переноса используют target.copy_foreign(source_object), добавление страниц или методы, которые выполняют копирование автоматически. Простые числа и строки можно создать заново, но словарь со ссылками требует обхода графа и сохранения связей.
make_indirect() превращает прямой объект в косвенный внутри документа. Это нужно для структур, которые спецификация требует хранить по ссылке, например некоторых деревьев и общих ресурсов. Создавать косвенными все словари без причины не следует: файл станет сложнее, а выгоды не будет. Наоборот, повторно используемый большой ресурс выгодно хранить один раз и ссылаться на него с нескольких страниц.
Не рекомендуется обходить дерево страниц через Root.Pages для обычных операций. Там действуют наследуемые атрибуты и промежуточные узлы; неправильное изменение счётчика или родителя повреждает структуру. Коллекция pdf.pages поддерживает согласованность и разворачивает наследование в ожидаемом виде.
Потоки и операторы содержимого
Stream сочетает словарь параметров и двоичные данные. read_raw_bytes() возвращает данные в сохранённом, возможно сжатом виде. read_bytes() применяет фильтры и возвращает декодированный поток. Для неизвестного фильтра или повреждённых данных возможны DependencyError и DataDecodingError. Размер декодированного потока заранее не всегда известен, поэтому непроверенные файлы обрабатывают с лимитами памяти.
Поток содержимого страницы содержит операнды и операторы PDF: матрицы, выбор шрифта, вывод текста, рисование путей и вызов XObject. parse_content_stream() разбирает его в инструкции, а unparse_content_stream() собирает обратно. Это позволяет найти операторы Do, изменить цвет, удалить конкретное рисование или провести аудит, не разбирая синтаксис регулярными выражениями.
from pikepdf import Pdf, Operator, parse_content_stream
with Pdf.open('input.pdf') as pdf:
for instruction in parse_content_stream(pdf.pages[0]):
if instruction.operator == Operator('Do'):
print('Вызван XObject:', instruction.operands[0])
Регулярное выражение по байтам опасно: токен может быть разделён между несколькими потоками, строка содержит экранирование, а двоичное inline-изображение допускает последовательности, похожие на операторы. Перед анализом можно вызвать contents_coalesce(), чтобы объединить массив потоков в один. Даже после этого нужен синтаксический парсер, а не поиск текста.
contents_add() добавляет байты или поток в начало либо конец содержимого. Это низкоуровневый способ записать графические команды, но код обязан балансировать q/Q, корректно объявлять ресурсы и учитывать текущую матрицу. Для большинства водяных знаков add_overlay() безопаснее, потому что он создаёт Form XObject и настраивает ресурсы.
TokenFilter может наблюдать или менять токены при чтении потока. Фильтр, добавленный через add_content_token_filter(), применяется лениво, когда содержимое будет прочитано или сохранено, и не снимается с открытого документа. Для разового анализа без изменения применяют get_filtered_contents(). Такой механизм подходит для потоковой замены операторов, но требует понимания грамматики PDF.
Исправление, нормализация и санитарная обработка
При открытии qpdf не доверяет слепо таблице перекрёстных ссылок и может восстановить позиции объектов по содержимому. Сообщения о повреждениях поступают через предупреждения и исключения. Если файл открылся, сохранение часто создаёт согласованную таблицу и объединяет цепочку инкрементальных обновлений в один файл. Это устраняет часть проблем, но не гарантирует исправление неверной семантики страниц, шрифтов или форм.
Для контролируемой проверки используют Job(['pikepdf', '--check', filename]).run() или соответствующий JSON job. Проверка сообщает о структурных ошибках, потоках и линейзации. В автоматизированном процессе исходник не перезаписывают сразу: результат сохраняют отдельно, затем повторно открывают, проверяют число страниц, метаданные и важные объекты, и только после успешной проверки заменяют рабочую копию.
Модуль sanitize предлагает целевые операции. Удаление JavaScript нейтрализует сценарии в известных местах действий. remove_external_access() удаляет URI, Launch, удалённые переходы, отправку и импорт форм, в том числе цепочки /Next. Видимые аннотации ссылок остаются, но действие исчезает. Отдельно можно удалить миниатюры и другие необязательные элементы.
Санитарные функции намеренно не удаляют все формы, XFA, аннотации, идентификаторы и метаданные: безусловная зачистка разрушила бы законное содержимое. Политику составляют из отдельных шагов. Например, входящую анкету можно оставить с AcroForm, убрать JavaScript и внешний доступ, удалить вложения неразрешённых типов и сохранить журнал найденных действий.
import pikepdf
from pikepdf import sanitize
with pikepdf.open('incoming.pdf') as pdf:
sanitize.remove_javascript(pdf)
sanitize.remove_external_access(pdf)
sanitize.remove_thumbnails(pdf)
pdf.save('incoming-sanitized.pdf')
Очистка не заменяет антивирус и песочницу. PDF может эксплуатировать ошибку конкретного просмотрщика через сложный шрифт, изображение или редкий фильтр, которые формально допустимы. Для непроверенных файлов ограничивают ресурсы процесса, обновляют qpdf и декодеры, отключают лишние зависимости и не отображают результат привилегированным приложением.
Сохранение и управление размером файла
Pdf.save() предлагает параметры, влияющие на совместимость, отладку и размер. compress_streams=True сжимает ранее несжатые потоки, но не перекодирует все уже сжатые данные. recompress_flate=True заставляет распаковать и заново сжать Flate-потоки; выигрыш зависит от исходного кодера и может сопровождаться заметной нагрузкой на процессор.
object_stream_mode управляет объектными потоками PDF 1.5. Режим generate обычно уменьшает файл, помещая подходящие объекты в компактные потоки. Режим disable разворачивает их для совместимости со старым или неисправным программным обеспечением. Preserve сохраняет принцип исходного документа. Если целевая система известна, выбор делают по её требованиям, а не только по размеру.
linearize=True создаёт Fast Web View: объекты располагаются так, чтобы просмотрщик мог начать показывать первую страницу до полной загрузки. Линейный файл часто немного больше, потому что содержит дополнительные таблицы подсказок. После последующего редактирования линейзация может стать недействительной; её выполняют на финальном шаге публикации.
normalize_content=True разбирает и форматирует потоки содержимого, что удобно для диагностики и сравнения, но не является обязательной оптимизацией. qdf=True создаёт режим QDF для ручного изучения структуры в текстовом редакторе. Такой файл не предназначен как компактная выдача пользователю; после ручных правок его возвращают в стандартный вид утилитой fix-qdf.
from pikepdf import Pdf, ObjectStreamMode
with Pdf.open('input.pdf') as pdf:
pdf.remove_unreferenced_resources()
pdf.save(
'optimized.pdf',
object_stream_mode=ObjectStreamMode.generate,
compress_streams=True,
recompress_flate=True,
linearize=True,
)
deterministic_id=True формирует идентификатор из детерминированных данных, что помогает воспроизводимым сборкам. Для зашифрованных файлов режим не работает, а полная байтовая воспроизводимость всё равно зависит от порядка входов, метаданных и параметров сжатия. Даты изменения и автоматически записываемые поля следует контролировать отдельно.
При записи в существующий путь механизм сначала использует временный файл в том же каталоге и затем заменяет назначение, что снижает риск потерять прежний результат при сбое. Если запись идёт в произвольный поток, забота о временном файле лежит на приложении. Обработчик прогресса получает значения от 0 до 100, но не должен читать или менять Pdf во время сохранения: это может повредить данные.
Сохранение не добавляет изменения в конец исходника, а сводит инкрементальные обновления в цельную запись. Это хорошо для нормализации и удаления осиротевших объектов, но меняет байты по всему файлу и ломает подписи, которые защищали прежние диапазоны. Также pikepdf не записывает результат поверх открытого входа без специального режима; безопаснее всегда использовать другой путь.
Job API и пакетные операции qpdf
Job открывает функции командной строки qpdf из Python без запуска отдельного процесса. Аргументы можно передать списком, начинающимся с условного имени программы, или словарём в формате Job JSON. Это удобно для проверок, шифрования, выбора страниц, работы с вложениями и других задач, уже выраженных параметрами qpdf.
from pikepdf import Job
Job(['pikepdf', '--check', 'document.pdf']).run()
Job({
'inputFile': 'document.pdf',
'outputFile': 'document-linear.pdf',
'linearize': '',
}).run()
Job уместен для высокоуровневой операции над целым документом. Объектная модель лучше подходит, когда решение зависит от содержимого конкретной страницы или метаданных. В одном конвейере можно сначала проверить файл Job, затем открыть Pdf, изменить страницы и снова выполнить проверку результата. Ошибочная конфигурация Job вызывает JobUsageError, а структурная ошибка входа — PDF-исключение или диагностическое сообщение.
JobBuilder предоставляет цепочечное формирование задания и уменьшает количество строковых опций. Он полезен в сервисе, где политика собирается из настроек: добавить вложение, выбрать страницы, зашифровать, линейризовать. Однако перед выполнением следует сохранять итоговую конфигурацию в журнал, иначе расследовать различие двух файлов будет трудно.

Пакетный конвейер для каталога документов
Надёжный пакетный обработчик разделяет обнаружение, проверку, преобразование и публикацию. Сначала формируется неизменяемый список входов и вычисляются контрольные суммы. Затем каждый файл открывается с ограничением времени и памяти, проверяется число страниц и наличие шифрования. Результат сохраняется во временный каталог, повторно открывается и только после успешной проверки перемещается в выходной каталог.
Исключения записывают с контекстом: путь, размер, стадия, тип исключения и краткое сообщение. Парольная ошибка, повреждённый PDF, неподдерживаемое изображение и ошибка диска требуют разных действий. Повторять все ошибки одинаково бессмысленно: неверный пароль не исправится повтором, а временный сбой файловой системы может исчезнуть.
from pathlib import Path
from pikepdf import Pdf, PdfError, PasswordError
for source in Path('incoming').glob('*.pdf'):
target = Path('processed') / source.name
try:
with Pdf.open(source) as pdf:
for page in pdf.pages:
page.rotate(0, relative=True)
pdf.remove_unreferenced_resources()
pdf.save(target, linearize=True)
with Pdf.open(target) as check:
if len(check.pages) == 0:
raise ValueError('Результат не содержит страниц')
except PasswordError:
print(source, 'нужен пароль')
except PdfError as exc:
print(source, 'ошибка PDF:', exc)
except OSError as exc:
print(source, 'ошибка файловой системы:', exc)
Для параллелизма безопаснее обрабатывать разные документы в отдельных процессах или независимых задачах. Один и тот же Pdf не следует менять из нескольких потоков. Параллельное декодирование больших изображений быстро исчерпывает память, поэтому число работников выбирают по пиковому размеру, а не по количеству ядер.
Если правило требует идентичного результата, сортируют словари входов, фиксируют параметры, избегают случайных имён ресурсов и включают детерминированный идентификатор. После этого сравнивают не только SHA-256 итоговых файлов, но и структурные свойства: количество страниц, размеры, метаданные, наличие шифрования и список вложений. Различный байтовый файл может быть функционально эквивалентным, а одинаковый размер ничего не доказывает.

Типовые ошибки и способы устранения
PdfError при открытии
PdfError означает, что механизм не смог корректно разобрать документ или выполнить операцию. Сначала проверяют, что файл начинается с PDF-заголовка, полностью скачан и не является HTML-страницей с расширением PDF. Затем запускают Job с --check и сохраняют диагностические предупреждения. Если файл открывается в просмотрщике, но не в pikepdf, это ещё не доказывает корректность: просмотрщик мог применять собственные эвристики.
При восстановлении не перезаписывают единственную копию. Если pikepdf открыл документ с предупреждениями, его сохраняют под новым именем и повторно открывают. Для критичных документов сравнивают визуальный результат, закладки, формы, вложения и подписи. Структурно читаемый результат может потерять семантику, которая уже была повреждена во входе.
PasswordError и ограничения шифрования
Если пароль известен, передают его как строку или байты в соответствии с API и не записывают в лог. Пустой пользовательский пароль иногда позволяет открыть файл без явного значения, но владельческий пароль при этом остаётся. После открытия проверяют, какой пароль совпал, прежде чем разрешать экспорт или изменение в приложении, которое добровольно соблюдает ограничения.
При миграции шифрования создают тестовый файл и открывают его минимум в двух целевых просмотрщиках. Старые программы могут не понимать сильный обработчик или Unicode-пароль. Снижение алгоритма ради совместимости ухудшает защиту, поэтому решение должно быть явным и документированным.
ForeignObjectError при переносе словаря или потока
Ошибка возникает, когда объект, связанный с одним владельцем, присваивается другому документу. Страницы переносит коллекция pages, а произвольный словарь копируют copy_foreign(). Если объект содержит ссылку на страницу, аннотацию или ресурс, после копирования проверяют, что ссылка указывает на целевую сущность, а не на ненужную копию.
Нельзя исправлять ошибку сериализацией repr() и повторным разбором: представление предназначено для диагностики, а не является полным PDF-синтаксисом. Правильный путь — использовать API копирования или создать новый объект с выбранными полями.
Изображение видно, но get_images() ничего не возвращает
Сначала убеждаются, что используется get_images(recursive=True). Рисунок может находиться внутри Form XObject, быть inline-изображением или состоять из векторных операторов. Для inline-данных разбирают поток содержимого и обрабатывают ContentStreamInlineImage. Для вектора извлечение как JPEG невозможно без рендеринга, которого pikepdf не выполняет.
Если ресурс найден, но extract_to() не поддерживает формат, читают параметры /Filter, /ColorSpace, /Decode, маски и размеры. Иногда исходный поток можно сохранить как есть, но он не будет самостоятельным изображением без заголовка. Преобразование через Pillow применяют только после оценки размера и наличия декодера.
После поворота изменился только просмотр, а координаты остались прежними
/Rotate задаёт преобразование отображения и не переписывает операторов. Это ожидаемо. Для приложения, которое анализирует координаты без учёта поворота, используют flatten_rotation(). После операции проверяют рамки страницы, аннотации и формы: координаты интерактивных объектов также должны соответствовать новому пространству.
Результат неожиданно вырос
Причины: линейзация, разворачивание объектных потоков, копирование общих ресурсов в каждый фрагмент, декодирование потоков, дублирование страниц и вложений. Сравнивают параметры save(), вызывают удаление неиспользуемых ресурсов и проверяют, не был ли один большой файл прикреплён дважды. Повторное сжатие Flate помогает не всегда; JPEG без перекодирования уже может быть близок к оптимальному размеру.
При разделении рост суммарного объёма естественен: каждый фрагмент получает собственные таблицы, шрифты и ресурсы. Если главная цель — доставка отдельных страниц, можно принять увеличение. Если цель — архив, выгоднее хранить единый PDF и индекс страниц отдельно.
Изменения метаданных не видны в просмотрщике
Просмотрщик может показывать DocumentInfo, XMP или кэшированное значение. Использование open_metadata() синхронизирует распространённые поля, но пользовательские схемы не обязаны отображаться. Закройте документ в просмотрщике, сохраните под новым именем и проверьте оба представления через pikepdf. Прямое редактирование только docinfo оставляет XMP прежним и создаёт конфликт.
Практические сценарии
Подготовка PDF к публикации на сайте
Конвейер удаляет опасные действия, проверяет вложения, задаёт корректные описательные метаданные, убирает неиспользуемые ресурсы и сохраняет с линейной загрузкой. Перед публикацией результат открывают заново, проверяют страницы и запускают структурную проверку. Если документ подписан, обработку согласуют с владельцем подписи, потому что сохранение изменит байтовое представление.
Для веб-доставки не следует автоматически понижать качество изображений: pikepdf не является растровым оптимизатором и сохраняет исходные потоки. Если нужен пересчёт изображений, их извлекают, осознанно перекодируют с контролем цветового профиля и заменяют либо используют специализированный оптимизатор. Простая линейзация ускоряет первый показ, но не уменьшает разрешение.
Сборка технического руководства из глав
Список глав задаётся конфигурацией. Код объединяет страницы, сохраняет максимальную требуемую версию PDF, создаёт закладки с начальными индексами, переносит только выбранные метаданные и прикладывает исходные данные как вложение. После сборки проверяются ссылки и нумерация. Если главы имеют формы, используется form-aware копирование, иначе одинаковые имена полей могут конфликтовать.

Создание вариантов одного документа
Один открытый Pdf можно сохранять несколько раз с разными параметрами, не изменяя его только самим фактом сохранения. Например, сформировать обычную копию, линейную копию и защищённую копию. Однако изменение страниц между сохранениями остаётся в памяти. Чтобы варианты расходились от одной базы предсказуемо, сначала выполняют общие правки, затем сохраняют варианты без дальнейшего изменения либо заново открывают базовый результат для каждой ветви.
from pikepdf import Pdf, Encryption
with Pdf.open('approved.pdf') as pdf:
pdf.save('approved-web.pdf', linearize=True)
pdf.save(
'approved-confidential.pdf',
encryption=Encryption(owner='owner-secret', user='reader-secret'),
)
pdf.save('approved-archive.pdf', preserve_pdfa=True)

Поиск подозрительных действий
Перед очисткой полезно составить отчёт: пройти по OpenAction, AA, аннотациям, полям, закладкам и цепочкам Next, записать тип каждого действия и его расположение. Затем политика решает, что удалить. Такой двухфазный подход лучше безусловной зачистки: команда безопасности видит, что именно было найдено, а пользователь может разрешить законные внутренние переходы.
После удаления внешнего доступа видимая ссылка может остаться без действия. Для пользовательского документа это иногда сбивает с толку. Можно дополнительно изменить оформление аннотации или удалить её целиком, но тогда изменится внешний вид. Решение зависит от задачи: санитарная копия для анализа может сохранять внешний вид, а публичная версия — удалять неработающие интерактивные элементы.
Сравнение pikepdf с аналогами
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| pikepdf | Исправления структуры, низкоуровневых правок, шифрования и пакетной сборки PDF в Python | Нет визуального редактора и собственного рендеринга страниц |
| pypdf | Простых операций с PDF в чистом Python и проектов без двоичной зависимости qpdf | Меньше возможностей для восстановления и объектной нормализации сложных файлов |
| PyMuPDF | Рендеринга страниц, извлечения текста, поиска, аннотаций и работы с геометрией | Лицензионные условия требуют отдельной проверки для выбранного способа распространения |
| qpdf | Командной проверки, преобразования структуры, шифрования и серверных сценариев без Python API | Сложные условные изменения неудобно выражать длинной командной строкой |
| PDF Commander | Ручного редактирования страниц и содержимого через понятные визуальные инструменты | Не предназначен для встраивания в Python-конвейер |
pikepdf выбирают, когда правило обработки должно быть воспроизводимым и требуется надёжная работа со структурой PDF. pypdf удобен для лёгкого чисто Python-проекта, PyMuPDF — когда центральны рендеринг и извлечение текста, qpdf — для готовых командных рецептов, а PDF Commander — когда документ правит человек и важна визуальная обратная связь. В сложном конвейере инструменты можно сочетать: pikepdf нормализует и собирает файл, а рендерер создаёт превью для контроля.
Ограничения, которые важно учитывать заранее
pikepdf не показывает страницу и не предоставляет мышью выделяемых объектов. Код не знает, что прямоугольник является кнопкой или что набор операторов визуально выглядит как таблица, пока приложение само не интерпретирует структуру. Поэтому ручная ретушь, выбор текста взглядом и перетаскивание страниц требуют визуального редактора или отдельного интерфейса поверх API.
Извлечение обычного текста и таблиц не является задачей pikepdf. Можно разобрать операторы Tj и TJ, но без обработки шрифтов, ToUnicode, позиционирования, порядка чтения и вложенных объектов получится неполный результат. Для текстового анализа применяют pdfminer.six, pdfplumber или PyMuPDF, а pikepdf оставляют для структуры, метаданных и записи.
Рендеринга в пиксели тоже нет. PdfImage извлекает растровые ресурсы, но не формирует изображение страницы с текстом, вектором, прозрачностью и слоями. Для превью нужен PDFium, MuPDF, Ghostscript или системный просмотрщик. Это разграничение полезно: документ можно менять без растрирования и потери поискового слоя.
Высокоуровневое создание сложной страницы ограничено. Модуль canvas умеет базовые операции, однако для генерации отчётов из HTML, шаблонов и таблиц лучше использовать ReportLab, WeasyPrint или другой генератор, а затем при необходимости обработать готовый PDF. pikepdf особенно силён там, где документ уже существует.
Соответствие PDF/A, PDF/X или PDF/UA нельзя доказать одним параметром сохранения. Изменение изображения, профиля, шрифта, метаданных или структуры тегов способно нарушить стандарт. После правки нужен профильный валидатор и, для доступности, проверка логического порядка, альтернативного текста и дерева структуры.
Проверочный список перед выдачей результата
- Открыть результат повторно и убедиться, что количество и порядок страниц ожидаемы.
- Проверить рамки MediaBox и CropBox, поворот и пользовательские метки страниц.
- Сравнить выбранные XMP-поля и не переносить чужие идентификаторы или заявления о стандартах.
- Проверить закладки, именованные назначения, аннотации и интерактивные поля.
- Убедиться, что вложения имеют ожидаемые имена, MIME-типы и отношения к документу.
- Проверить шифрование в целевых просмотрщиках и не считать запреты печати криптографической защитой.
- Запустить структурную проверку и просмотреть предупреждения qpdf.
- При публикации проверить активные действия, JavaScript, внешние переходы и запуск файлов.
- Для архивного формата выполнить независимую проверку соответствия.
- Сохранить журнал входной и выходной контрольных сумм, параметров и исключений.
Такой контроль особенно важен потому, что PDF допускает несколько способов представить одно и то же. Страница может выглядеть правильно, но содержать неработающую ссылку; метаданные могут расходиться; скрытый ресурс может увеличить файл; декларативный поворот может сбить координаты. Проверка должна сочетать структуру, семантику и визуальный результат.
Ответы на практические вопросы
Можно ли изменить исходный файл на месте?
Технически предусмотрен режим, разрешающий перезапись входа, но безопаснее сохранять в другой путь. pikepdf читает объекты лениво, поэтому преждевременная запись поверх источника способна лишить его данных, которые ещё не были прочитаны. Отдельный результат также упрощает откат, сравнение и проверку. После успешной валидации приложение может атомарно заменить рабочий файл.
Сохраняется ли качество страниц при объединении?
Перенос страниц не растрирует их и не перекодирует изображения автоматически, поэтому вектор, текст и исходные сжатые потоки обычно сохраняются. Качество изменится, только если код явно декодирует и заменяет ресурсы либо применяет внешний оптимизатор. Размер может измениться из-за перестройки объектов и удаления недостижимых данных.
Можно ли вытащить все фотографии без потерь?
Многие JPEG и другие поддерживаемые потоки извлекаются без повторного кодирования. Но видимая фотография может включать маску, цветовое преобразование, несколько тайлов или векторные элементы. Все фотографии нельзя определить только по типу XObject. Результат проверяют визуально и учитывают вложенные формы, inline-изображения и маски.
Почему сохранённый файл отличается по SHA-256, хотя правок не было?
При сохранении заново строятся таблицы, порядок и упаковка объектов, идентификатор, служебные поля и сжатие. Инкрементальные обновления сводятся в одну запись. Поэтому байтовое равенство не ожидается. Для воспроизводимого процесса фиксируют параметры и детерминированный идентификатор, но сравнение функций документа всё равно важнее одного хеша.
Можно ли применять pikepdf к сканам?
Да, если задача касается страниц, изображений, метаданных, шифрования или структуры. Распознавание текста библиотека не выполняет. OCR-система может использовать pikepdf для проверки входа, переноса исходного изображения и добавления готового текстового слоя, но само распознавание делает отдельный движок.
Что делать с предупреждениями при открытии?
Записать их в журнал, сохранить восстановленную копию под новым именем и проверить её повторным открытием. Предупреждение означает, что вход отклонялся от ожидаемой структуры и была применена эвристика. Для массового процесса можно разрешить известные безопасные предупреждения, но новые типы следует анализировать, а не скрывать.
Можно ли удалить скрытые данные?
Сохранение исключает объекты, недостижимые от корня, но скрытая информация может оставаться достижимой в метаданных, вложениях, аннотациях, слоях, формах, альтернативных изображениях и потоках. Полная политика очистки должна перечислять эти места. Простое удаление страницы или обрезка CropBox не гарантирует удаления содержимого.
Как проверить, что водяной знак не изменил остальную страницу?
Сравнить ресурсы и потоки до и после, убедиться, что добавлен только Form XObject и короткий вызов, а затем отрендерить обе страницы внешним движком и выполнить визуальное сравнение. Для прозрачности и наложений проверяют несколько просмотрщиков, потому что ошибки графического состояния могут проявляться по-разному.
Тестирование кода, который меняет PDF
Автоматические тесты не должны ограничиваться проверкой существования выходного файла. Минимальный тест повторно открывает результат, сравнивает число страниц, проверяет ожидаемые рамки и повороты, читает выбранные метаданные и убеждается, что нужное вложение доступно по имени. Для шифрования тест открывает файл правильным и неправильным паролем. Для закладок он проходит outline-дерево и проверяет цели. Так ошибки в структуре обнаруживаются до визуальной проверки.
Полезно хранить небольшой набор специально подобранных PDF: пустой документ, одну страницу, документ с объектными потоками, повреждённой xref-таблицей, формой, аннотациями, вложениями, пользовательскими метками страниц, шифрованием и несколькими типами изображений. Случайный корпоративный отчёт не покрывает редкие ветви. Тестовые файлы должны иметь понятное происхождение и лицензию, а ожидаемые свойства — быть зафиксированы в коде, а не только на глаз.
Для визуальной регрессии pikepdf дополняют рендерером. Страницы исходника и результата превращают в изображения с одинаковым DPI, затем сравнивают пиксели с допуском. Это выявляет исчезнувшие шрифты, неверную матрицу и непрозрачный водяной знак. Пиксельное сравнение не заменяет структурный тест: два документа могут выглядеть одинаково, но один потеряет текстовый слой, ссылку или вложение.
Структурный снимок удобно строить в JSON: версия PDF, число страниц, размеры и поворот каждой страницы, список фильтров потоков, имена вложений, ключевые XMP-поля и типы действий. Снимок не должен включать нестабильные идентификаторы и даты, иначе тест будет падать без функционального изменения. Для расследования сохраняют отдельный полный дамп qpdf, но не используют его как единственное условие успеха.
Работа с потоками памяти и файловыми объектами
Когда PDF приходит по сети или из базы данных, его можно открыть через io.BytesIO, если поток поддерживает seek и tell. Для больших файлов копирование всех байтов в память невыгодно; лучше использовать временный файл или поток, который предоставляет произвольный доступ. Выход также можно записать в BytesIO, но при исключении буфер может содержать частичный файл. Приложение должно отвергать такой буфер и не отправлять его клиенту.
from io import BytesIO
from pikepdf import Pdf
source_buffer = BytesIO(received_bytes)
result_buffer = BytesIO()
with Pdf.open(source_buffer) as pdf:
del pdf.pages[-1]
pdf.save(result_buffer, linearize=True)
result_bytes = result_buffer.getvalue()
В веб-приложении ограничивают размер загрузки до открытия и дополнительно контролируют распакованный объём изображений и потоков. Сам PDF может быть небольшим, но содержать объект, который после декодирования занимает гигабайты. Тайм-аут уровня HTTP не всегда останавливает вычисления внутри процесса; для недоверенных файлов надёжнее отдельный рабочий процесс с лимитом памяти и времени.
При сохранении в сетевую файловую систему атомарная замена может иметь другие гарантии, чем на локальном диске. Безопасный шаблон записывает результат рядом с назначением, синхронизирует данные, проверяет повторным открытием и только затем выполняет переименование. Если каталог смонтирован через объектное хранилище, перемещение может быть копированием, поэтому публикацию лучше строить через уникальные ключи и указатель на готовую версию.
Производительность и расход памяти
Быстродействие зависит не столько от количества страниц, сколько от структуры ресурсов и выбранной операции. Перестановка страниц может быть быстрой даже в большом документе, потому что не требует декодировать изображения. Рекомпрессия потоков, извлечение через Pillow, санитарный обход графа и разбор каждого content stream заметно дороже. Перед оптимизацией измеряют отдельные стадии и не включают recompress_flate только из предположения, что он всегда уменьшает файл.
Повторное открытие одного источника для каждой страницы создаёт лишнюю работу. При разделении документ открывают один раз и последовательно формируют фрагменты. При объединении источники держат открытыми до переноса страниц, но закрывают сразу после завершения. Если одновременно открываются сотни файлов, можно исчерпать лимит дескрипторов; очередь должна ограничивать число активных источников.
Копирование страниц между документами переносит достижимые ресурсы. Если один шрифт используется во всех источниках как отдельный объект, итог может содержать несколько копий, потому что автоматическое семантическое дедуплицирование шрифтов небезопасно. Уменьшение такого файла требует специализированной оптимизации и проверки, что объекты действительно эквивалентны. Совпадение имени шрифта или размера потока недостаточно.
Для прогресса сохранения передают callback, который только обновляет внешнее состояние. Внутри него нельзя обращаться к страницам, менять метаданные или запускать второе сохранение. В графическом интерфейсе callback отправляет число в потокобезопасную очередь, а основной UI читает её. В сервере прогресс записывается в хранилище задачи с ограниченной частотой, чтобы сотни обновлений не стали дороже самой записи PDF.
Кодировки строк и имён
PDF Name, текстовая строка и байтовый поток кодируются по разным правилам. pikepdf регистрирует кодек pdfdoc для PDFDocEncoding, но современный текст часто хранится в Unicode-форме. Нельзя считать, что bytes.decode('utf-8') корректно обработает любую PDF-строку. Высокоуровневые свойства обычно возвращают Python-строки, а низкоуровневый аудит должен учитывать тип объекта и исходное представление.
Имена ресурсов вроде /Im0 — не пользовательский текст. Специальные байты в Name экранируются через решётку и шестнадцатеричный код. Создание через Name() безопаснее ручной конкатенации слеша. При генерации случайных имён методом add_resource() можно задать префикс, например Im, но не следует полагаться на конкретное получившееся имя между запусками.
Имена вложений отображаются пользователю и могут содержать Unicode, однако получатель может извлечь их в файловую систему с другими правилами. Перед массовой выгрузкой нормализуют имя, удаляют разделители пути, зарезервированные имена и управляющие символы. При этом внутренний ключ вложения и фактическое имя файла могут различаться; приложение должно явно выбрать, какое значение использовать.
Создание пустых страниц и простого содержимого
add_blank_page(page_size=(width, height)) создаёт страницу с MediaBox и пустым потоком. Размер задаётся в PDF-единицах. Пустая страница полезна как холст для N-up, подложки или Form XObject. Если размеры берутся из исходной страницы, копируют эффективный MediaBox и учитывают UserUnit; иначе визуально одинаковые размеры могут дать различный физический лист.
Модуль canvas предоставляет базовые средства рисования и текста, но не заменяет систему верстки. Для штампа из нескольких линий и короткой подписи его достаточно. Для многоязычного текста, переноса строк, таблиц и сложных шрифтов проще создать одностраничный PDF генератором, а затем наложить его. Это разделяет задачи: генератор отвечает за внешний вид, pikepdf — за точное внедрение в существующий документ.
При добавлении собственного потока нужно объявить каждый шрифт, XObject, ColorSpace и ExtGState в ресурсах страницы. add_resource() добавляет объект в нужный раздел и возвращает имя. Ручное присваивание в Resources допустимо, но требует создать отсутствующие словари и не затереть существующие ключи. После удаления команд проверяют, какие ресурсы больше не используются.
Диагностика структуры без изменения файла
Для первичного аудита документ открывают только на чтение и не вызывают save. Можно вывести pdf.trailer, Root, свойства шифрования, список страниц и предупреждения. show_xref_table() пишет таблицу перекрёстных ссылок в логгер с уровнем INFO и полезен при сопоставлении объекта с байтовой позицией. Сам pikepdf не полагается на xref как на абсолютную истину и пересчитывает её при записи.
Отладочный вывод объектов может быть очень большим и содержать персональные данные. Журнал ограничивают выбранными ключами, хешами потоков и длинами, а не полными байтами. Для вложений и метаданных применяют маскирование. Ошибка анализа не должна приводить к тому, что защищённый договор целиком окажется в системном логе.
Когда требуется сравнить два PDF, сначала нормализуют условия: одинаково декодируют или не декодируют потоки, игнорируют даты и ID, сортируют ключи. QDF облегчает чтение человеком, но не превращает PDF в простой текстовый формат. Косвенные номера объектов могут измениться без изменения смысла, поэтому различие номера не считается дефектом само по себе.
Проектирование собственного инструмента поверх pikepdf
Командная утилита должна отделять параметры пользователя от операций API. Аргументы проверяются до открытия: существование пути, допустимые углы, диапазоны страниц, конфликт входа и выхода. Затем формируется план и печатается в режиме dry-run. Такой режим особенно полезен для удаления страниц и вложений: пользователь видит, что будет изменено, до записи нового файла.
Для графической оболочки pikepdf можно использовать как движок, но превью должен создавать отдельный рендерер. Модель хранит путь, список страниц и запланированные операции; интерфейс не меняет Pdf при каждом движении мыши. После подтверждения операции применяются в фиксированном порядке к свежему открытию источника. Это облегчает отмену и предотвращает накопление промежуточных преобразований.
В серверном API не следует принимать произвольный Python-код. Разрешённые действия описывают схемой: выбрать страницы, повернуть, удалить метаданные, добавить заданный штамп, зашифровать. Каждый параметр валидируется, а выполнение происходит от непривилегированного пользователя. Такой дизайн использует гибкость pikepdf внутри сервиса, не отдавая клиенту доступ к файловой системе и процессу.
Формат конфигурации должен различать номер страницы для человека и индекс Python. Хорошая схема использует явные поля page_number с отсчётом от единицы либо page_index с нуля и запрещает двусмысленность. Диапазон проверяется после открытия, потому что число страниц заранее неизвестно. Для пользовательских меток предусматривают отдельный поиск и обработку совпадающих меток.
Итоговый рабочий подход
Лучший результат получается, когда задача описана как последовательность проверяемых преобразований: открыть и проверить вход, выбрать страницы или объекты, внести минимальные изменения, удалить только доказанно ненужные ресурсы, сохранить с осознанными параметрами, заново открыть и валидировать. Такой процесс использует сильные стороны pikepdf — точную объектную модель и механизм qpdf — и не пытается заменить ими OCR, рендеринг или визуальную верстку.
Для одноразового ручного исправления удобнее графический редактор. Для регулярно повторяемого правила код быстро окупается: одинаковая обработка применяется к каждому файлу, параметры хранятся рядом с проектом, результат можно тестировать, а ошибки — воспроизводить. pikepdf особенно полезен в конвейерах публикации, архивации, подготовки датасетов, санитарной очистки и сборки документов, где структура PDF важнее визуального управления мышью.
