Как обрезать поля PDF локально с помощью Python
Выберите отступы, измените видимую границу страницы и проверьте каждую страницу новой копии. Обрезка не удаляет скрытые данные.
Чтобы страница удобнее помещалась на экране
Из-за широких полей текст PDF может казаться мелким, когда просмотрщик помещает страницу целиком на экран планшета. Этот способ позволяет локально, с помощью Python, убрать из видимой области заданную ширину с каждого края всех страниц. Получится отдельная копия для чтения. Скрипт не ищет пустые поля автоматически: их величину выбираете вы после просмотра документа.
Обрезка меняет границу видимой области страницы — CropBox. Она не увеличивает буквы внутри PDF, не перевёрстывает абзацы и не удаляет скрытое содержимое. Не используйте этот способ, чтобы скрыть личные данные. Границу страницы можно восстановить, а текст за ней — извлечь. Размер файла может почти не измениться или даже увеличиться.
Сначала выберите величину полей
Подойдёт обычный незашифрованный PDF с правильно ориентированными страницами, без форм и цифровых подписей. Скрипт откажется обрабатывать страницы, у которых ненулевой поворот в метаданных или UserUnit отличается от 1. Страница может выглядеть правильно именно потому, что просмотрщик применяет поворот из метаданных; такой случай этот способ не охватывает. Скрипт также не разворачивает содержимое, нарисованное боком внутри страницы.
Просмотрите разные страницы: обложку, сплошной текст, иллюстрации, сноски, чётные и нечётные полосы. Если в просмотрщике есть линейка, измерьте самую узкую пустую полоску у каждого края. Иначе начните с небольшой пробы, например 2 мм, и оцените результат. Оставляйте немного места вокруг текста. Одинаковая обрезка может срезать номер страницы с другой вёрсткой; для документа, где каждой странице нужны свои границы, этот способ не подходит.
Пример: копия страницы A4 для чтения
Возьмём страницу размером 210 × 297 мм. Уберём из видимой области по 10 мм слева и справа, по 5 мм сверху и снизу. Новая ширина составит 210 − 10 − 10 = 190 мм, высота — 297 − 5 − 5 = 287 мм. Прямоугольник размером 100 × 30 мм в учебном файле останется размером 100 × 30 мм: меняется только граница страницы.
Если исходный левый нижний угол имеет координаты (0, 0), новый прямоугольник идёт от (10, 5) до (200, 292) мм. Здесь PDF использует 72 единицы на дюйм: миллиметры нужно умножить на 72 ÷ 25,4. Получаем примерно (28,35; 14,17; 566,93; 827,72) единицы. Скрипт учитывает и ненулевое начало координат, и уже заданную обрезку: отступы откладываются от общей области CropBox и MediaBox, а не обязательно от нуля.
Скачать учебный PDF формата A4
Оригинальный одностраничный образец с английскими подписями. Сохраните его как input.pdf для команды ниже. Уберите по 10 мм слева и справа, по 5 мм сверху и снизу; прямоугольник 100 × 30 мм должен сохранить размер.
Подготовьте рабочую папку
Нужен Python версии 3.9 или новее. Создайте папку, положите туда копию PDF под именем input.pdf и сохраните полный скрипт ниже в обычный текстовый файл crop_pdf.py. Откройте терминал в этой папке. Оригинал храните отдельно. В командах используется pypdf 6.21.2 — версия, с которой проверен этот пример.
В macOS или Linux выполните две команды подготовки. Для установки библиотеки нужен интернет; сам скрипт обрезки никуда не загружает ваш PDF.
Установите pypdf в отдельное окружение
sh
python3 -m venv .venv-pdf-crop
.venv-pdf-crop/bin/python -m pip install "pypdf==6.21.2"Имена команд в Windows
В Windows создайте окружение командой py -3 -m venv .venv-pdf-crop. В командах установки и запуска замените .venv-pdf-crop/bin/python на .venv-pdf-crop\Scripts\python.exe. Активировать окружение не нужно.
Сохраните как crop_pdf.py
python
import argparse
import io
import math
from pathlib import Path
from pypdf import PdfReader, PdfWriter
from pypdf.generic import RectangleObject
parser = argparse.ArgumentParser(description="Crop visible PDF margins in mm.")
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
for edge in ("left", "right", "top", "bottom"):
parser.add_argument(f"--{edge}", type=float, default=0)
args = parser.parse_args()
trims = [args.left, args.right, args.top, args.bottom]
if any(not math.isfinite(v) or v < 0 for v in trims):
parser.error("Margins must be finite, nonnegative millimetres.")
left, right, top, bottom = [v * 72 / 25.4 for v in trims]
reader = PdfReader(args.input, strict=True)
if reader.is_encrypted:
parser.error("Encrypted PDFs are outside this recipe.")
root = reader.trailer["/Root"]
if "/AcroForm" in root or "/Perms" in root:
parser.error("Forms and signed/restricted PDFs are outside this recipe.")
if not reader.pages:
parser.error("The PDF has no pages.")
boxes = []
for number, page in enumerate(reader.pages, 1):
if page.rotation != 0 or float(page.get("/UserUnit", 1)) != 1:
parser.error(f"Page {number}: requires rotation 0 and UserUnit 1.")
media = [float(v) for v in page.mediabox]
crop = [float(v) for v in page.cropbox]
if any(not math.isfinite(v) for v in media + crop):
parser.error(f"Page {number}: invalid page coordinates.")
x0, y0 = max(media[0], crop[0]), max(media[1], crop[1])
x1, y1 = min(media[2], crop[2]), min(media[3], crop[3])
box = (x0 + left, y0 + bottom, x1 - right, y1 - top)
if box[0] >= box[2] or box[1] >= box[3]:
parser.error(f"Page {number}: margins leave no visible page.")
boxes.append(box)
writer = PdfWriter(clone_from=reader)
for page, box in zip(writer.pages, boxes):
page.cropbox = RectangleObject(box)
buffer = io.BytesIO()
writer.write(buffer)
with args.output.open("xb") as output:
output.write(buffer.getvalue())
print(f"Created {args.output} with {len(boxes)} pages. Check every page.")Скрипт проверяет все страницы перед созданием нового выходного файла.
Обрежьте поля из примера
sh
.venv-pdf-crop/bin/python crop_pdf.py "input.pdf" "cropped.pdf" --left 10 --right 10 --top 5 --bottom 5Проверьте копию и подберите отступы
Откройте cropped.pdf в привычном просмотрщике и сравните каждую страницу с оригиналом. Проверьте все четыре края, номера страниц, сноски, нужные ссылки, комментарии и закладки. Число страниц должно сохраниться. В примере с A4 инструмент, показывающий размеры CropBox, должен сообщить 190 × 287 мм; MediaBox по-прежнему описывает исходную страницу.
Чтобы изменить отступ, снова запускайте скрипт для input.pdf и задавайте новое имя результата. Если взять cropped.pdf, поля будут отложены ещё раз от уже обрезанной границы. Ошибка FileExistsError означает, что выбранный выходной файл существует: выберите другое имя. Отрицательные, бесконечные и чрезмерные отступы не допускаются. Если команда завершилась с ошибкой или была прервана, не используйте возможный неполный результат; проверьте его либо перед повтором удалите только этот новый файл.
При одинаковом масштабе просмотра буквы и отметки в обоих PDF останутся одного размера. Режим «По ширине» может показать обрезанную страницу крупнее, поскольку поля занимают меньше места. Печать с подгонкой под лист тоже может изменить масштаб; если важны физические размеры, печатайте с проверкой масштаба 100%. Копия для чтения не заменяет оригинал, не очищает документ от личных данных и не гарантирует одинаковую работу всех специальных возможностей PDF.
Что мы проверили
Мы выполнили скрипт в Linux с Python 3.12.3 и pypdf 6.21.2. MuPDF и Poppler отрисовали пример с видимой областью 190 × 287 мм и неизменным прямоугольником 100 × 30 мм. Четыре подписи на полях исчезли из видимой области, но их по-прежнему удалось извлечь как текст. Также проверены ненулевое начало координат, уже обрезанные и многостраничные файлы, отказ на неподдерживаемых входных данных и существующий выходной файл. Команды в Windows и macOS, физическую печать и все возможные функции PDF мы не проверяли.