2025, Oct 05 21:16

Как повернуть страницу PDF в PyMuPDF на произвольный угол без растеризации

Показываем, как в PyMuPDF повернуть страницу PDF на любой, даже 2,5°, угол с сохранением текста и векторов: show_pdf_page, примеры, важные ограничения.

Повернуть страницу PDF на произвольный угол — задача встречается чаще, чем кажется, но очевидный интерфейс PyMuPDF словно упирается в стену. Стандартные переключатели поворота поддерживают только кратные 90 градусам и лишь меняют ориентацию просмотра. Если нужен наклон, например на 2,5 градуса, при этом с сохранением выделяемого текста, векторной графики и изображений, есть аккуратный «родной» способ — одним вызовом.

Когда очевидный путь не срабатывает

Логично попытаться применить матрицу трансформации прямо к странице. Вы открываете документ, выбираете страницу, сбрасываете её поворот в ноль, строите Matrix, предварительно поворачиваете её и пробуете применить к содержимому страницы. Подводный камень: применить матрицу ко всей странице таким образом нельзя — такого API для страниц нет.

import pymupdf as pm
pdf_obj = pm.open("Sample.pdf")  # открыть входной PDF
first_pg = pdf_obj[0]             # обратиться к первой странице
first_pg.set_rotation(0)          # только 0/90/180/270, уровень ориентации
xform = pm.Matrix(1, 0, 0, 1, 0, 0)
xform.prerotate(2.5)              # целимся в 2,5 градуса
first_pg.add_transformation(xform)  # такого метода для страниц не существует
pdf_obj.save("Sample_rotated.pdf")

Почему это не работает

Флаг поворота страницы в PDF не вращает реальное содержимое. Он хранит значение ориентации, которое учитывают просмотрщики, и ограничен 0, 90, 180 и 270 градусами. Попытка задать произвольное преобразование для самой страницы упирается в ограничения API, потому что такой операции нет как страничной трансформации в PyMuPDF. Если нужен тонкий угол вроде 2,5 градуса, понадобится иной подход, который при этом сохраняет текст и векторные данные.

Простое и рабочее решение в PyMuPDF

PyMuPDF умеет отрисовать страницу на другой странице с заданной трансформацией. Метод, который даёт произвольный поворот, — show_pdf_page. Он переносит исходную страницу в прямоугольник целевой страницы и применяет параметр rotate — в результате сохраняются выделяемый текст и векторное содержимое.

import pymupdf  # НЕ fitz
# подготовить пустую принимающую страницу
sheet = pymupdf.open().new_page(width=595, height=841)
# импортировать страницу 0 из input.pdf в приёмник, повернув на 2,5 градуса
sheet.show_pdf_page(
    pymupdf.Rect(0, 0, 595, 841),
    pymupdf.open("input.pdf"),
    pno=0,
    rotate=2.5,
    keep_proportion=True
)
# сохранить новый документ
sheet.parent.save("output.pdf")

Такой подход сохраняет векторы, корректно работает со сканами и оставляет текст выделяемым. Преобразование выполняется во время импорта, поэтому содержимое не растеризуется.

Чего ожидать и какие ограничения

Метод нацелен на «ядро» страницы. Поэтому не все сопутствующие элементы обязательно сохранятся. Метаданные могут передаваться не полностью, так как выход формируется заново. Аннотации, закладки и ссылки могут исчезнуть — как и в других библиотеках, которые перестраивают содержимое страниц: исходные координаты после трансформированного импорта могут перестать быть валидными. Точный поворот также способен игнорировать прежнюю ротацию исходной страницы, поэтому при импорте может понадобиться подстроить ориентацию для источников в альбомной раскладке или со смешанными поворотами. Есть и бонус: этот приём может удалить встроенный JavaScript, что полезно с точки зрения безопасности, хотя это подтверждено не для всех файлов. В экосистеме API есть и другие функции, например morph, которые дают родственные преобразования, но чаще ориентированы на отдельные типы объектов. Путь через show_pdf_page — удачный баланс для обработки всей страницы без покопного редактирования каждого объекта.

Чистое «распрямление» и санитизация

Тем же способом можно «свести» поворот к нулю и «санитизировать» документ, переимпортируя только ядро содержимого страниц. Это помогает нормализовать PDF, убрать лишнее и заодно сжать файл.

import pymupdf  # НЕ fitz
out_pdf = pymupdf.open()            # документ-приёмник
in_pdf = pymupdf.open("input.pdf")  # исходный документ
for idx in range(len(in_pdf)):      # обойти страницы источника
    canvas = out_pdf.new_page()     # по умолчанию — A4 в портретной ориентации
    # импортировать «ядро» страницы с поворотом 0.0 градуса (фактически распрямление)
    canvas.show_pdf_page(canvas.rect, in_pdf, idx, rotate=0.0)
# при желании очистить мусор и сжать основные потоки
out_pdf.save("output.pdf", garbage=3, deflate=True)

Почему это важно знать

Возможность поворачивать на дробные градусы без растеризации сохраняет качество и доступность PDF. Текст остаётся выделяемым, векторы — векторами, а размер файла — компактным. Переимпорт страниц одновременно даёт практичный способ нормализовать документы: убрать встроенные скрипты, избавиться от хрупких «надстроек» и обойти странности, накопившиеся от прежних правок.

Итоги

Поворот страницы кратный 90 — это флаг ориентации; точного наклона он не даёт. Когда нужен сдвиг на 2,5 градуса или любой другой тонкий угол с сохранением структуры PDF, импортируйте страницу в новую с помощью show_pdf_page и параметра rotate. Помните, что вы по сути заново собираете ядро страниц, и часть метаданных и интерактивных элементов может не перенестись. Если такой компромисс вас устраивает, вы получаете компактное, точное и удобное в автоматизации решение целиком на Python и PyMuPDF.

Статья основана на вопросе с StackOverflow от theozh и ответе K J.