В одном из своих проектов я столкнулся с задачей динамического добавления пароля к PDF файлу для его защиты. Чтобы этого добиться, было решено использовать библиотеку PDFI
от Setasign, а точнее класс FpdiProtection
. Эта статья рассказывает о том, как это можно сделать, а также дает ответ на вопрос, как обойти ограничение бесплатной библиотеки.
Прежде всего нам понадобится сама библиотека. Для этого устанавливаем ее с помощью composer:
composer require setasign/fpdf "~1.8"
composer require setasign/fpdi-protection "~2.0"
Во время инсталляции также composer установит setasign/fpdi
.
Создание класса помощника
Чтобы упростить защиту PDF файлов, создадим класс помощник, который назовем PasswordProtectPDF
, который приведен ниже. Этот класс будет выполнять импорт каждой страницы из исходного PDF файла в новый и сохранять его по указанному пути.
<?php
namespace App\Helper;
use setasign\Fpdi\Fpdi;
use setasign\Fpdi\PdfReader;
use setasign\FpdiProtection\FpdiProtection;
class PasswordProtectPDF
{
protected $pdf = null;
public function __construct($file, $user_pass, $master_pass = null)
{
$this->protect($file, $user_pass, $master_pass);
}
public function protect($file, $user_pass, $master_pass = null)
{
$pdf = new FpdiProtection();
# указываем путь PDF файла, который защищаем
$pagecount = $pdf->setSourceFile($file);
# копируем все страница исходного файла
for ($i = 1; $i <= $pagecount; $i++) {
$tplidx = $pdf->importPage($i);
$specs = $pdf->getTemplateSize($tplidx);
# устанавливаем правильную ориентация, ширину и высоту
$pdf->addPage($specs['orientation'], [ $specs['width'], $specs['height'] ]);
$pdf->useTemplate($tplidx);
}
# устанавливаем пользовательский и мастер пароль
# мастер пароль позволяет открыть полный доступ к PDF файлу
$pdf->setProtection([
FpdiProtection::PERM_PRINT,
FpdiProtection::PERM_DIGITAL_PRINT
], $user_pass, $master_pass);
# устанавливает имя приложения как создатель
$pdf->SetCreator("My Company ltd.");
$this->pdf = $pdf;
return $this;
}
public function setTitle($title)
{
# указываем название документа, отображаемое в окне
$this->pdf->SetTitle($title);
return $this;
}
/**
* I = Встроенный PDF
* S = PDF в виде строки, которую вы можете обрабатывать вручную (например, с ручными заголовками для принудительного рендеринга или загрузки)
* D = Принудительная загрузка PDF-файла на устройство пользователя с использованием второго параметра в качестве имени файла.
* F = Сохранение PDF-файла на сервер в виде файла с использованием второго параметра в качестве пути и имени файла.
*/
public function output($name, $type = 'I')
{
# выводим документ
if ($type === 'S') {
return $this->pdf->Output($type, $name);
} else {
$this->pdf->Output($type, $name);
}
return $this;
}
# cохранить PDF как файл на сервере
public function saveAsFile($path)
{
$this->pdf->Output('F', $path);
}
}
Пример использования
Как видно из кода класса, он позволяет сохранить защищенный файл различными способами. Конкретно в моем случае защиту PDF файлов нужно было встроить в Magento 2 магазин, и при каждом заказе файл защищался уникальным паролем.
<?php
require "PasswordProtectPDF.php";
function generatePassword() {
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
$pass = array(); //remember to declare $pass as an array
$alphaLength = strlen($alphabet) - 1; //put the length -1 in cache
for ($i = 0; $i < 8; $i++) {
$n = rand(0, $alphaLength);
$pass[] = $alphabet[$n];
}
return implode($pass); //turn the array into a string
}
function saveProtectedPdfAs($file,$protectedFile, $password) {
try {
$pdf = new PasswordProtectPDF($file, $password);
$pdf->setTitle(basename($file));
$pdf->saveAsFile($protectedFile);
} catch (\Exception $e) {
if ($e->getCode() == 267) {
throw new \Exception('Компрессия данного файла не поддерживается.');
}
throw new \Exception('Ошибка генерации PDF файла.');
}
}
$input = 'magazine.pdf';
$password = generatePassword();
saveProtectedPdfAs($input, 'magazine_protected.pdf', $password);
Возможные проблемы и их решение
Как видите функция saveProtectedPdfAs
содержит конструкцию try...catch и это неспроста. Библиотека setasign/fpdi
, которая используется для чтения чтения PDF файла поставляется в виде бесплатной и платной версии. Бесплатная версия всем хороша и выполняет свой функционал, но только для PDF файлов версией не выше 1.4. Платная же версия лишена данного недостатка, но цена ее кусается немного - 100 EUR персональная лицензия и 400 EUR лицензия для команды. Одним словом покупка лицензии была нецелесообразной для одного лишь проекта, а заказчик делал упор на то, что версии файлов могут быть разные.
Немного погуглив возможные решения, я остановился на наиболее простом и приемлемом с моей точки зрения - использование ghostscript
для конвертирования PDF файлов (тем более он уже был установлен на сервере). Добавляем совсем немного кода и получаем желаемый результат:
function convertPdf($file) {
$backup = $file . '.back';
@copy($file, $backup);
# convert PDF file to version 1.4 using ghostscript
shell_exec( "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dNOPAUSE -dQUIET -dBATCH -sOutputFile={$file} {$backup}");
}
Вызываем данную функцию перед защитой файла:
<?php
$input = 'magazine.pdf';
$password = generatePassword();
convertPdf($input); # конвертируем исходные PDF в поддерживаемую версию
saveProtectedPdfAs($input, 'magazine_protected.pdf', $password); # защищаем файл паролем
Заключение
Приведенный в данной статье код может быть не оптимальным, но достаточен для обучающих целей. Если статья помогла решить проблему и сохранила время, значит она была написана не зря =)
Не стесняйтесь, задавайте вопросы, пишите предложения в комментариях.