Сжатие PDF за 5 шагов: пример от 80 МБ до 8 МБ

Вы получаете отсканированный PDF-договор размером 80 МБ. Он возвращается из электронной почты, отклоняется мессенджерами и превышает лимиты загрузки в каждой системе, которую вы пробуете. Знакомо? PDF-файлы становятся большими не просто так, и безопасное уменьшение их до десятой части размера — это не магия, а вопрос понимания того, что находится внутри контейнера, и применения правильной техники к каждому типу содержимого. В этой статье разбираются пять ключевых методов сжатия из спецификации PDF, а затем пошагово рассматривается реальный отсканированный договор с 80 МБ до 8,2 МБ.

1. Откуда берётся размер PDF

PDF — это контейнерный формат. Внутри он представляет собой набор объектов: дерево страниц, словари ресурсов, потоки содержимого, XObjects изображений, шрифты и метаданные. Разрастание размера почти всегда сводится к трём категориям.

Встроенные изображения. Это основная статья расходов в отсканированных документах. Сканер с настройкой по умолчанию 600 DPI создаёт растровое изображение страницы A4 размером примерно 4960×7016 пикселей. При 24-битной глубине цвета сырые данные одной страницы составляют около 100 МБ; даже после сжатия без потерь FlateDecode (zlib) каждая страница весит от 2 до 5 МБ. Договор на 30 страниц достигает 60–150 МБ.

Встроенные шрифты. Для гарантии идентичного отображения на всех устройствах PDF встраивает полные файлы шрифтов. Полный CJK-шрифт (например, гарнитура Source Han) содержит более 20 000 глифов, и один файл TTF весит 10–20 МБ; OTF может быть ещё больше. Встройте несколько начертаний или несколько семейств, и эта категория быстро разрастается.

Векторная графика и избыточные объекты. Сами векторы невелики, но сложные CAD-экспорты, вложенные поля форм, ресурсы из удалённых ревизий и дублированные потоки метаданных накапливаются незаметно. Механизм инкрементного обновления PDF — печально известный виновник: каждое сохранение добавляет новую ревизию, не удаляя старые объекты, поэтому файл растёт при каждом редактировании.

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

2. Пятиступенчатый метод вкратце

PDF compression 5-step method flowchart: image resampling, format conversion, font subsetting, redundancy removal, linearization

Шаг 1: Передискретизация изображений

Это единственный наиболее эффективный шаг для отсканированных документов. Скан с разрешением 600 DPI неотличим от 150 DPI на любом обычном экране, но количество пикселей различается в 16 раз (DPI линейно; пиксели квадратично, поэтому (600/150)² ≈ 16).

Ключевое решение — целевой DPI. Содержимое документов (договоры, отчёты, счета) хорошо выглядит при 150 DPI. Для печати повысьте до 200–300 DPI. Архивные сканы высокой точности могут сохранять 400 DPI и выше. Для алгоритма передискретизации рекомендуется Lanczos — он лучше сохраняет резкость краёв текста при уменьшении, чем билинейная интерполяция, и избегает блочности метода ближайшего соседа.

При реализации вы читаете /Width, /Height и информацию о DPI из /DecodeParms XObject изображения, масштабируете пропорционально и записываете обратно. Обязательно обновляйте /Width, /Height и /Intent вместе, иначе отображение будет смещено.

Шаг 2: Преобразование формата изображения

PDF отмечает кодирование изображения полем /Filter. Отсканированные документы обычно используют /FlateDecode (без потерь в стиле PNG), что гораздо менее эффективно, чем /DCTDecode (JPEG), для реальных изображений, таких как фотографии или отсканированные страницы.

Тот же скан A4, который занимает 4 МБ под FlateDecode, может снизиться до 300–500 КБ как JPEG качества 72 — сокращение на 85 % без заметной разницы на экране. Две вещи, за которыми следить: преобразуйте чёрно-белые документы в оттенки серого (/ColorSpace /DeviceGray) перед кодированием, чтобы снова вдвое уменьшить размер, и повысьте качество JPEG до 80+ на страницах с подписями, чтобы штрихи пера оставались чёткими.

/DCTDecode — не единственный вариант. Для диаграмм с большими плоскими цветными областями /JPXDecode (JPEG 2000) даёт меньший размер при эквивалентном качестве, хотя совместимость со старыми программами-читалками хуже.

Шаг 3: Подстановка шрифтов

Встраивание полного CJK-шрифта стоит 10 МБ и более, но 30-страничный договор может фактически использовать лишь 800–1 500 различных символов. Подстановка шрифтов оставляет только те глифы, которые встречаются в документе, и отбрасывает все остальные.

Шрифт с подмножеством обычно сжимается до 100–300 КБ — сокращение более чем на 95 %. Подход: пройдите по потоку содержимого каждой страницы, извлеките символы из операторов Tj и TJ, разрешите их через таблицу /ToUnicode, чтобы получить множество используемых кодовых точек, затем используйте инструмент шрифтов (например, fontTools) для создания нового файла шрифта, содержащего только эти глифы.

Два предостережения: CID-шрифты требуют обработки /CIDToGIDMap во избежание смещения глифов, а шрифты с подмножеством по соглашению переименовываются с префиксом (например, ABCDEF+), чтобы отметить их как подмножества, что не влияет на отображение.

Шаг 4: Удаление избыточных объектов

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

Логика очистки: начните от каталога документа (/Root), выполните обход в глубину дерева страниц и цепочки ссылок на ресурсы, отметьте каждый достижимый объект, затем перестройте таблицу перекрёстных ссылок и отбросьте недостижимые. Этот шаг особенно эффективен для документов, редактировавшихся много раз: один проход обычно удаляет 5 %–15 % размера, а в крайних случаях накопленных инкрементных обновлений — более 30 %.

Шаг 5: Линеаризация

Линеаризация не уменьшает общее количество байт, но реорганизует структуру файла для включения быстрого веб-просмотра (Fast Web View). Линеаризованный PDF помещает объекты, необходимые для первой страницы, в начало файла и вставляет таблицы подсказок, чтобы программа-читалка могла отрисовать первую страницу до завершения загрузки.

Хотя линеаризация не уменьшает файл напрямую, это естественный завершающий шаг конвейера сжатия. В сочетании со сжатием потоков объектов и потоками перекрёстных ссылок общая реструктуризация всё же даёт небольшое снижение размера (1 %–3 %). Что ещё важнее, линеаризованные PDF заметно лучше работают в сценариях веб-встраивания и облачного предпросмотра.

3. Сравнение до и после

Вот как изменяется договор размером 80 МБ после каждого шага:

PDF compression before/after file size comparison: 80MB scanned contract compressed to 8MB effect demonstration

Шаг 1 (передискретизация) даёт наибольшее падение — с 80 МБ прямо до 15,2 МБ. Шаг 2 (преобразование формата) убирает ещё 4 МБ. Оставшиеся три шага вместе срезают около 7 МБ. Общий коэффициент сжатия составляет примерно 9,8:1.

4. Псевдокод на Python

Приведённый ниже псевдокод показывает полную логику из пяти шагов. В рабочей среде вы бы объединили библиотеки вроде pikepdf, PyMuPDF и reportlab.


import io
from PIL import Image

def compress_pdf(input_path, output_path,
                 target_dpi=150, jpeg_quality=72, grayscale=True):
    """
    Точка входа пятиступенчатого сжатия PDF.
    :param input_path: путь к входному PDF
    :param output_path: путь к выходному PDF
    :param target_dpi: целевой DPI (по умолчанию 150, подходит для экрана)
    :param jpeg_quality: качество JPEG (по умолчанию 72)
    :param grayscale: преобразовать в оттенки серого (рекомендуется для ч/б документов)
    :return: (исходный_размер, сжатый_размер)
    """
    pdf = open_pdf(input_path)
    original_size = file_size(input_path)

    # Шаг 1: передискретизация изображений (600DPI -> целевой DPI)
    for page in pdf.pages:
        for image in page.images:
            resampled = resample_image(image, target_dpi)
            image.replace(resampled)

    # Шаг 2: преобразование формата (FlateDecode -> DCTDecode/JPEG)
    for page in pdf.pages:
        for image in page.images:
            if image.filter == "FlateDecode":
                jpeg_data = encode_jpeg(image, jpeg_quality, grayscale)
                image.replace(jpeg_data, filter="DCTDecode")

    # Шаг 3: подстановка шрифтов (оставить только используемые глифы)
    used_chars = collect_used_chars(pdf)
    for font in pdf.fonts:
        subset = build_subset(font, used_chars)
        font.replace(subset)

    # Шаг 4: удалить избыточные объекты
    pdf.remove_unreferenced_resources()
    pdf.compact_xref()  # перестроить таблицу перекрёстных ссылок

    # Шаг 5: линеаризация (Fast Web View)
    pdf.save(output_path, linearize=True,
             object_streams=True, compress_streams=True)
    pdf.close()

    compressed_size = file_size(output_path)
    return original_size, compressed_size


def resample_image(image, target_dpi):
    """Передискретизация изображения до целевого DPI."""
    src_dpi = image.dpi
    if src_dpi <= target_dpi:
        return image  # никогда не увеличивать, пропустить
    scale = target_dpi / src_dpi
    new_w = int(image.width * scale)
    new_h = int(image.height * scale)
    pil_img = Image.open(io.BytesIO(image.data))
    return pil_img.resize((new_w, new_h), Image.LANCZOS)


def encode_jpeg(image, quality, grayscale):
    """Преобразовать в оттенки серого и закодировать как JPEG."""
    pil_img = Image.open(io.BytesIO(image.data))
    if grayscale and pil_img.mode != "L":
        pil_img = pil_img.convert("L")  # в оттенки серого
    buf = io.BytesIO()
    pil_img.save(buf, format="JPEG", quality=quality, optimize=True)
    return buf.getvalue()


def collect_used_chars(pdf):
    """Обойти потоки содержимого и собрать все фактически используемые кодовые точки."""
    used = set()
    for page in pdf.pages:
        for text_op in page.content_stream.text_ops:
            used.update(extract_codepoints(text_op))
    return used


def build_subset(font, used_chars):
    """Построить шрифт с подмножеством, содержащий только используемые глифы."""
    subset = font.subset(glyphs=used_chars)
    subset.name = prefix + "+" + font.name  # соглашение об именовании подмножеств
    return subset

Это псевдокод — обработка ошибок, сопоставление CID-шрифтов и реконструкция ToUnicode опущены — но каркас ясен: передискретизация, преобразование, подмножество, очистка, линеаризация, в таком порядке.

5. Пример: отсканированный договор с 80 МБ до 8,2 МБ

Исходный документ — реальный китайский договор на 32 страницы, отсканированный при 600 DPI, исходный размер 80,4 МБ.

Профиль документа:

  • 32 страницы, A4, чёрно-белый скан 600 DPI
  • 32 встроенных изображения, все FlateDecode, в среднем 2,4 МБ на страницу
  • 2 встроенных шрифта (Source Han Serif Regular + Bold), суммарно 28 МБ
  • Сохранён инкрементно 3 раза, присутствуют избыточные объекты

Шаги и параметры:

ШагОперацияКлючевые параметрыРазмер
1Передискретизация изображений600 DPI → 150 DPI, Lanczos80,4 → 15,2 МБ
2Преобразование форматаFlateDecode → JPEG, качество 72, оттенки серого15,2 → 11,0 МБ
3Подстановка шрифтовоставить 1 287 используемых глифов11,0 → 10,5 МБ
4Удаление избыточностиперестроить таблицу перекрёстных ссылок10,5 → 9,8 МБ
5ЛинеаризацияObject Stream + Fast Web View9,8 → 8,2 МБ

Результат: 8,2 МБ, соотношение 9,8:1. Текст чёткий и разборчивый, подписи intact, границы таблиц не имеют разрывов, файл печатается без проблем.

6. Рекомендации по DPI для разных сценариев

Целевой DPI — основной рычаг, балансирующий сжатие и разборчивость. В таблице ниже приведены рекомендуемые значения для распространённых случаев использования.

Случай использованияРекомендуемый DPIРазмер изображения на страницуПримечания
Чтение с экрана / веб-предпросмотр150150–300 КБЧёткий текст, быстрая загрузка, подходит для e-mail
Стандартная печать (A4 лазер/струйный)200–300400–800 КБЧёткая печать; 300 для договоров
Архивное резервное копирование (высокая точность)400–6001–3 МБСохраняет детали скана для долгосрочного хранения
Только чёрно-белый текст150–20080–200 КБКрайне малый после оттенков серого, подходит для пакетного архивирования
Цветной документ с фотографиями200–300500 КБ–1,2 МБКачество JPEG 75–85, баланс цвета и размера

Практическое правило: по умолчанию 150 DPI для экрана, 300 DPI только при печати, и храните отдельный мастер-файл с высоким DPI для архивирования. Не пытайтесь сделать один файл 600 DPI пригодным для всех целей.

7. Часто задаваемые вопросы

В1: Сжатый файл получился больше оригинала. Почему?

Обычно это происходит при повторном сжатии PDF, уже содержащего маленькие изображения, закодированные в JPEG. Передискретизация и перекодирование имеют накладные расходы: если изображение уже JPEG 150 DPI, «передискретизация» до 150 DPI не уменьшит его, а новый кодировщик JPEG может быть менее эффективным, чем оригинальный. Добавьте накладные расходы метаданных от реструктуризации потоков объектов, и файл может немного вырасти. Решение: перед сжатием проверьте текущий DPI и кодирование каждого изображения и пропустите изображения, уже соответствующие цели (≤150 DPI и JPEG). Обрабатывайте только объекты, действительно превышающие порог.

В2: Сжатие ухудшит качество изображения?

Да, но его можно сохранить визуально незаметным. Сжатие с потерями происходит в основном на шаге 2 (кодирование JPEG). При качестве 72 разница незаметна на экране; при печати вы можете заметить лёгкий эффект окантовки на высокочастотных деталях, таких как тонкие линии или мелкие шрифты. Если качество важнее, повысьте качество JPEG до 85 или переключитесь на режим без потерь JPEG 2000 (/JPXDecode с обратимым преобразованием). Подстановка шрифтов, удаление избыточности и линеаризация выполняются без потерь и не оказывают влияния на отображение.

В3: Можно ли сжать уже сжатый PDF повторно?

Да, но с убывающей отдачей. Первый проход собирает самые доступные плоды (высокий DPI, кодирование FlateDecode, полные шрифты). Второму проходу почти нечего оптимизировать. Для оценки: проверьте, превышают ли изображения целевой DPI, остаётся ли кодирование, отличное от JPEG, и являются ли шрифты всё ещё полными наборами. Если все три уже оптимальны, дальнейшее сжатие опирается в основном на удаление избыточности и линеаризацию, обычно экономя лишь 5 %–10 %. Не сжимайте повторно уже оптимизированный PDF — каждое кодирование с потерями накапливает потерю качества.

8. Краткий справочник параметров

ПараметрРекомендуемое значениеПримечания
Целевой DPI (экран)150Стандарт для содержимого документов
Целевой DPI (печать)300Сохраняет чёткость печати A4
Качество JPEG (экран)70–75Сначала размер, разборчивость достаточна
Качество JPEG (печать)80–90Сначала качество, размер вторичен
Цветовой режим (ч/б документ)DeviceGrayВдвое меньше размера vs цвет
Подстановка шрифтовВключеноОбязательно для CJK-шрифтов, сокращение 95 %+
ЛинеаризацияВключеноУлучшает опыт веб-предпросмотра
Object StreamВключеноСжимает метаданные объектов
Очистка избыточностиВключеноОсобенно эффективно для многократно редактируемых файлов

Заключение

Сжатие PDF — это не чёрная магия, а целенаправленная обработка каждого типа объектов внутри файла. Пятиступенчатый метод следует чёткой логике: сначала обрабатывается крупнейший вкладчик (изображения, через передискретизацию и преобразование формата), затем шрифты (подстановка), затем структура (удаление избыточности и линеаризация). Запустите этот конвейер на типичном отсканированном договоре 80 МБ, и вы надёжно получите 8–10 МБ с полной сохранностью читаемости и печатаемости.

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

Связанные статьи:

Нужно сжать файлы? Попробуйте SmartSlim

На базе собственного движка сжатия на Rust поддерживается 10 основных категорий и 40+ форматов, включая PDF/изображения/видео/Office/OFD. Локальная обработка — данные не покидают вашу инфраструктуру.