• Познакомьтесь с пентестом веб-приложений на практике в нашем новом бесплатном курсе

    «Анализ защищенности веб-приложений»

    🔥 Записаться бесплатно!

  • CTF с учебными материалами Codeby Games

    Обучение кибербезопасности в игровой форме. Более 200 заданий по Active Directory, OSINT, PWN, Веб, Стеганографии, Реверс-инжинирингу, Форензике и Криптографии. Школа CTF с бесплатными курсами по всем категориям.

Статья Сжатие PDF тремя библиотеками и сравнение результатов с использованием Python

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

000.jpg

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


Что потребуется?

Для начала давайте установим PDFNetPython3. Это обертка для PDFTron SDK. К сожалению, она не является бесплатной, но предлагает два типа лицензий, в зависимости от того, какой продукт вы разрабатываете — коммерческий или для собственного, некоммерческого использования. Конечно же, мы воспользуемся бесплатной лицензией. Плюсом данной библиотеки является то, что она не использует для сжатия PDF внешние утилиты, такие, например, как Ghostscript. Для ее установки пишем в терминале:

pip install PDFNetPython3

После того, как библиотека будет установлена, нужно получить демо-ключ. Для этого перейдите на страницу получения , нажмите кнопку «Reveal» и скопируйте сгенерированный ключ. Он понадобиться для инициализации библиотеки. Ключ должен быть скопирован полностью, с приставкой «demo».

Следующая библиотека, которую мы будем использовать, это уже известная PyPDF2. Помимо разнообразных операций с PDF-файлами она также может выполнять их сжатие. А вот насколько, это мы выясним в процессе. Для установки библиотеки пишем в терминале:

pip install PyPDF2

И еще один скрипт, который, по сути, является самостоятельным решением и его можно использовать как есть из командной строки — это PDF Compressor. В своем скрипте я буду импортировать модуль в скрипт, а точнее его функцию compress. Для его использования нужно скачать файл в папку со скриптом. PDF Compressor использует Ghostscript для своей работы. А потому, если он не установлен у вас в системе, то желательно пройти на страницу , скачать исполняемый файл для вашей системы и установить. Если же вы используете Linux, то вполне возможно, что Ghostscript установлен уже по умолчанию. А потому не нужно делать лишних операций. Если же это не так, можно установить его командой (для систем на базе Debian):

sudo apt install ghostscript

Теперь, когда все подготовительные операции выполнены, можно импортировать библиотеки и модули в наш скрипт.

Python:
import os

import PyPDF2
from PDFNetPython3.PDFNetPython import PDFDoc, Optimizer, SDFDoc, PDFNet

from pdf_compressor import compress


Функция преобразования размера файла

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

Python:
def get_size_format(b):
    for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
        if b < 1024:
            return f"{b:.2f} {unit}B"
        b /= 1024
    return f"{b:.2f} YB"


Сжатие с помощью PDFNetPython3

Создадим функцию для сжатия PDF с помощью библиотеки PDFNetPython3. Я назвал ее compress_file_PDFTron(path_f: str), чтобы было понятно, с помощью какой библиотеки происходит сжатие файлов. На входе данная функция получает путь к файлу в строковом формате. Затем из переданного пути к файлу получаем имя файла, убираем расширение и формируем новое имя для сохранения сжатого файла. Также, для вычисления процента сжатия файлов считываем стартовый размер pdf.

Python:
    compres_file = f'{os.path.split(path_f)[-1][0:-4]}_pdftron.pdf'
    start_size = os.path.getsize(path_f)

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

Python:
        PDFNet.Initialize('demo:1657') # ваш демо-ключ
        doc = PDFDoc(path_f)
        doc.InitSecurityHandler()
        Optimizer.Optimize(doc)

И сохраняем сжатый документ с помощью функции Save. Первый аргумент — это путь и имя сжатого файла. А второй аргумент, согласно документации — побитовая дизъюнкция флагов, используемых в качестве опций, во время сериализации. Если заглянуть в код, то можно увидеть, что таких аргументов несколько. Подробнее о них можно почитать . И закрываем документ.

Python:
        doc.Save(os.path.join(os.path.split(path_f)[0], compres_file), SDFDoc.e_linearized)
        doc.Close()

Так как во время сжатия файлов могут возникать исключения, желательно их обработать. Поэтому заключаем код в блок try — except, и в случае возникновения исключения сообщаем об этом пользователю, после чего закрываем открытый документ. Ну, а далее считывается размер файла после сжатия, вычисляется процент и эта информация выводить в терминал. Также, возвращаем процент сжатия и размера сжатого файла.

Python:
def compress_file_PDFTron(path_f: str):
    compres_file = f'{os.path.split(path_f)[-1][0:-4]}_pdftron.pdf'
    start_size = os.path.getsize(path_f)
    try:
        PDFNet.Initialize('demo:1657019238652:7a4868db0300000000ca217f1b3d2bd71920f00ff3b6f178a4c0003863')
        doc = PDFDoc(path_f)
        doc.InitSecurityHandler()
        Optimizer.Optimize(doc)
        doc.Save(os.path.join(os.path.split(path_f)[0], compres_file), SDFDoc.e_linearized)
        doc.Close()
    except Exception as e:
        print("Ошибка сжатия файла: ", e)
        doc.Close()
        return
    compress_size = os.path.getsize(os.path.join(os.path.split(path_f)[0], compres_file))
    compression_ratio = 1 - (compress_size / start_size)
    print(f'\n[+] Сжатие PDFTron')
    print(f'[+] Файл: {os.path.split(path_f)[-1]}')
    print(f'   - [~] Размер файла до сжатия: {get_size_format(start_size)}')
    print(f'   - [~] Размера файла после сжатия: {get_size_format(compress_size)}')
    print(f'   - [~] Коэффициент сжатия: {compression_ratio:.3%}\n')
    return compression_ratio, get_size_format(compress_size)


Сжатие с помощью PyPDF2

Создадим еще одну функцию для сжатия PDF, но уже с использованием другой библиотеки, ведь наша цель — это сравнение степени сжатия документов. Я назвал функцию compress_file_PyPDF2(path_f: str). На вход она принимает путь к исходному файлу.

Затем формируем имя, которое будет использоваться для сохранения сжатого файла. Считываем размер оригинального. Создаем объект PyPDF2.PdfWriter, с помощью которого будем записывать сжатый документ, а также объект PyPDF2.PdfReader, в который передаем путь к исходному файлу. С его помощью документ будет открыт и прочитан.

Python:
    out_name = f'{os.path.split(path_f)[-1][0:-4]}_pypdf2.pdf'
    start_size = os.path.getsize(path_f)
    writer = PyPDF2.PdfWriter()
    reader = PyPDF2.PdfReader(path_f)

Затем в цикле от 0 до количества страниц в документе открываем каждую страницу, обрабатываем с помощью функции сжатия compressContentStreams() и передаем в объект writer для последующей записи. После чего, записываем сжатый файл в байтовом режиме на диск.

Python:
    for num in range(0, reader.numPages):
        print(f'\r[~] Обработка {num + 1}/{reader.numPages}', end='')
        page = reader.getPage(num)
        page.compressContentStreams()
        writer.addPage(page)
    with open(os.path.join(os.path.split(path_f)[0], out_name), 'wb') as file:
        writer.write(file)

Далее, как и в предыдущей функции, считываем размер сжатого файла, вычисляем коэффициент сжатия и выводим информацию в терминал. Размер сжатого файла и коэффициент возвращаем из функции.

Python:
def compress_file_PyPDF2(path_f: str):
    out_name = f'{os.path.split(path_f)[-1][0:-4]}_pypdf2.pdf'
    start_size = os.path.getsize(path_f)
    writer = PyPDF2.PdfWriter()
    reader = PyPDF2.PdfReader(path_f)
    for num in range(0, reader.numPages):
        print(f'\r[~] Обработка {num + 1}/{reader.numPages}', end='')
        page = reader.getPage(num)
        page.compressContentStreams()
        writer.addPage(page)
    with open(os.path.join(os.path.split(path_f)[0], out_name), 'wb') as file:
        writer.write(file)

    compress_size = os.path.getsize(os.path.join(os.path.split(path_f)[0], out_name))
    compression_ratio = 1 - (compress_size / start_size)
    print(f'\n\n[+] Сжатие PyPDF2')
    print(f'[+] Файл: {os.path.split(path_f)[-1]}')
    print(f'   - [~] Размер файла до сжатия: {get_size_format(start_size)}')
    print(f'   - [~] Размера файла после сжатия: {get_size_format(compress_size)}')
    print(f'   - [~] Коэффициент сжатия: {compression_ratio:.3%}\n')
    return compression_ratio, get_size_format(compress_size)


Сжатие файлов с помощью модуля PDF Compressor и Ghostscript

Создадим функцию, с помощью которой будем сжимать файлы с использованием Ghostscript. Назову ее compress_file_pdfcompressor(path_f: str). На вход она также принимает путь к файлу pdf, а на выходе возвращает коэффициент сжатия и размер сжатого файла. Для использования модуля нужно три параметра. Первый — это путь к сжимаемому файлу, второй — путь или название выходного файла, третий — степень сжатия.

У степени сжатия есть пять значений: 0 — качество по умолчанию, 1 — предпечатная подготовка, 2 — печать, 3 — электронная книга, 4 — экран. Соответственно, чем выше будет степень сжатия, тем хуже будет качество выходного документа. Я выставил значение 3 — то есть, электронная книга. Путем небольших экспериментов удалось определить, что данная степень сжатия позволяет читать документы в более-менее приемлемом качестве. Если поставить 4, картинки становятся уже пикселизированными и не особо читаемыми.

Для начала определяем размер файла. Затем формируем путь для сохранения выходного файла вместе с его названием. После чего вызываем функцию compress, куда и передаем вышеперечисленные параметры.

Python:
    start_size = os.path.getsize(path_f)
    out_file = os.path.join(os.path.split(path_f)[0], f'{os.path.split(path_f)[-1][0:-4]}_pdfcompressor.pdf')
    compress(path_f, out_file, power=3)

А далее, как и в предыдущих функциях считываем размер сжатого файла, вычисляем коэффициент сжатия и выводим информацию в терминал. После чего возвращаем коэффициент сжатия и размер сжатого файла из функции.

Python:
def compress_file_pdfcompressor(path_f: str):
    start_size = os.path.getsize(path_f)
    out_file = os.path.join(os.path.split(path_f)[0], f'{os.path.split(path_f)[-1][0:-4]}_pdfcompressor.pdf')
    compress(path_f, out_file, power=3)
    compress_size = os.path.getsize(os.path.join(os.path.split(path_f)[0],
                                                 f'{os.path.split(path_f)[-1][0:-4]}_pdfcompressor.pdf'))
    compression_ratio = 1 - (compress_size / start_size)
    print(f'\n[+] Сжатие PDF Compressor')
    print(f'[+] Файл: {os.path.split(path_f)[-1]}')
    print(f'   - [~] Размер файла до сжатия: {get_size_format(start_size)}')
    print(f'   - [~] Размера файла после сжатия: {get_size_format(compress_size)}')
    print(f'   - [~] Коэффициент сжатия: {compression_ratio:.3%}')
    return compression_ratio, get_size_format(compress_size)


Код запуска скрипта

А теперь, когда функции для работы скрипта созданы, осталось написать блок кода, в котором они и будут вызываться. Я не стал делать отдельной функции, а просто вызвал их в блоке if __name__ …

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

Python:
if __name__ == "__main__":
    path_file = input('[~] Введите путь к PDF-файлу: ')
    if not os.path.exists(path_file):
        print('[-] Файл по указаному пути отсутствует')
    if not path_file.endswith(".pdf"):
        print('[-] Неподдерживаемый формат')
        exit(0)
    proc_tron = compress_file_PDFTron(path_file)
    proc_pypdf2 = compress_file_PyPDF2(path_file)
    proc_pdfcompr = compress_file_pdfcompressor(path_file)

    print('\n[+] Итоговая информация')
    print(f'[+] Название файла: "{os.path.split(path_file)[-1]}"')
    print(f'   - Оригинальный размер: {get_size_format(os.path.getsize(path_file))}')
    print(f'    - Сжатие PDFTron: {proc_tron[0]:.3%}')
    print(f'    - Размер файла после сжатия: {proc_tron[1]}')
    print(f'    - Сжатие PyPDF2: {proc_pypdf2[0]:.3%}')
    print(f'    - Размер файла после сжатия: {proc_pypdf2[1]}')
    print(f'    - Сжатие PDF Compressor: {proc_pdfcompr[0]:.3%}')
    print(f'    - Размер файла после сжатия: {proc_pdfcompr[1]}')


Сравнение сжатия файлов разными модулями и алгоритмами

Для начала я выбрал файл достаточно большого размера: 270 Мб. Данный pdf собран из изображений и текстовый слой в нем отсутствует. Лучше всего в сжатии данного типа документов показал себя PDF Compressor. От сжал файл аж на 81 процент, против смешных цифр у других алгоритмов.

compress01.png

Сжатие pdf без текстового слоя

Второй файл, на котором производились тесты текстовый слой уже имел. И его размер был значительно меньше: 18 Мб. И в этом тесте лучший процент сжатия был у PDFTron, который сжал документ на 47 процентов.

compress02.png

Сжатие pdf с текстовым слоем

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

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

Python:
# pip install PDFNetPython3
# pip install PyPDF2
# sudo apt install ghostscript

import os

import PyPDF2
from PDFNetPython3.PDFNetPython import PDFDoc, Optimizer, SDFDoc, PDFNet

from pdf_compressor import compress


def get_size_format(b):
    for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
        if b < 1024:
            return f"{b:.2f} {unit}B"
        b /= 1024
    return f"{b:.2f} YB"


def compress_file_PDFTron(path_f: str):
    compres_file = f'{os.path.split(path_f)[-1][0:-4]}_pdftron.pdf'
    start_size = os.path.getsize(path_f)
    try:
        PDFNet.Initialize('demo:1657019238652:7a4868db0300000000ca217f1b3d2bd71920f00ff3b6f178a4c0003863')
        doc = PDFDoc(path_f)
        doc.InitSecurityHandler()
        Optimizer.Optimize(doc)
        doc.Save(os.path.join(os.path.split(path_f)[0], compres_file), SDFDoc.e_linearized)
        doc.Close()
    except Exception as e:
        print("Ошибка сжатия файла: ", e)
        doc.Close()
        return
    compress_size = os.path.getsize(os.path.join(os.path.split(path_f)[0], compres_file))
    compression_ratio = 1 - (compress_size / start_size)
    print(f'\n[+] Сжатие PDFTron')
    print(f'[+] Файл: {os.path.split(path_f)[-1]}')
    print(f'   - [~] Размер файла до сжатия: {get_size_format(start_size)}')
    print(f'   - [~] Размера файла после сжатия: {get_size_format(compress_size)}')
    print(f'   - [~] Коэффициент сжатия: {compression_ratio:.3%}\n')
    return compression_ratio, get_size_format(compress_size)


def compress_file_PyPDF2(path_f: str):
    out_name = f'{os.path.split(path_f)[-1][0:-4]}_pypdf2.pdf'
    start_size = os.path.getsize(path_f)
    writer = PyPDF2.PdfWriter()
    reader = PyPDF2.PdfReader(path_f)
    for num in range(0, reader.numPages):
        print(f'\r[~] Обработка {num + 1}/{reader.numPages}', end='')
        page = reader.getPage(num)
        page.compressContentStreams()
        writer.addPage(page)
    with open(os.path.join(os.path.split(path_f)[0], out_name), 'wb') as file:
        writer.write(file)

    compress_size = os.path.getsize(os.path.join(os.path.split(path_f)[0], out_name))
    compression_ratio = 1 - (compress_size / start_size)
    print(f'\n\n[+] Сжатие PyPDF2')
    print(f'[+] Файл: {os.path.split(path_f)[-1]}')
    print(f'   - [~] Размер файла до сжатия: {get_size_format(start_size)}')
    print(f'   - [~] Размера файла после сжатия: {get_size_format(compress_size)}')
    print(f'   - [~] Коэффициент сжатия: {compression_ratio:.3%}\n')
    return compression_ratio, get_size_format(compress_size)


def compress_file_pdfcompressor(path_f: str):
    start_size = os.path.getsize(path_f)
    out_file = os.path.join(os.path.split(path_f)[0], f'{os.path.split(path_f)[-1][0:-4]}_pdfcompressor.pdf')
    compress(path_f, out_file, power=3)
    compress_size = os.path.getsize(os.path.join(os.path.split(path_f)[0],
                                                 f'{os.path.split(path_f)[-1][0:-4]}_pdfcompressor.pdf'))
    compression_ratio = 1 - (compress_size / start_size)
    print(f'\n[+] Сжатие PDF Compressor')
    print(f'[+] Файл: {os.path.split(path_f)[-1]}')
    print(f'   - [~] Размер файла до сжатия: {get_size_format(start_size)}')
    print(f'   - [~] Размера файла после сжатия: {get_size_format(compress_size)}')
    print(f'   - [~] Коэффициент сжатия: {compression_ratio:.3%}')
    return compression_ratio, get_size_format(compress_size)


if __name__ == "__main__":
    path_file = input('[~] Введите путь к PDF-файлу: ')
    if not os.path.exists(path_file):
        print('[-] Файл по указаному пути отсутствует')
    if not path_file.endswith(".pdf"):
        print('[-] Неподдерживаемый формат')
        exit(0)
    proc_tron = compress_file_PDFTron(path_file)
    proc_pypdf2 = compress_file_PyPDF2(path_file)
    proc_pdfcompr = compress_file_pdfcompressor(path_file)

    print('\n[+] Итоговая информация')
    print(f'[+] Название файла: "{os.path.split(path_file)[-1]}"')
    print(f'   - Оригинальный размер: {get_size_format(os.path.getsize(path_file))}')
    print(f'    - Сжатие PDFTron: {proc_tron[0]:.3%}')
    print(f'    - Размер файла после сжатия: {proc_tron[1]}')
    print(f'    - Сжатие PyPDF2: {proc_pypdf2[0]:.3%}')
    print(f'    - Размер файла после сжатия: {proc_pypdf2[1]}')
    print(f'    - Сжатие PDF Compressor: {proc_pdfcompr[0]:.3%}')
    print(f'    - Размер файла после сжатия: {proc_pdfcompr[1]}')

А на этом, пожалуй, все.

Спасибо за внимание. Надеюсь, что данная информация будет вам полезна
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!