ivanilsonsousa / agnostic-pdf
Manipulação, assinatura digital e verificação de PDFs com integração opcional para Laravel
Requires
- php: ^8.2
- ext-mbstring: *
- ext-openssl: *
- ext-zlib: *
- dompdf/dompdf: ^3.1
- mpdf/mpdf: ^8.2
- papier/papier: ^3.0
Requires (Dev)
- fakerphp/faker: ^1.23
- friendsofphp/php-cs-fixer: ^3.85
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.5 || ^11.0
- phpunit/phpunit: ^12.3
Suggests
- laravel/framework: Required only for the optional Laravel service provider, facade, views and HTTP responses (^12.0 || ^13.0).
Provides
None
Conflicts
None
Replaces
None
README
Manipulação de PDFs com drivers intercambiáveis (mPDF e Dompdf), assinatura digital incremental e integração opcional com Laravel. Fornece uma API para renderizar, clonar, comprimir, assinar e verificar PDFs.
Foco: DX simples no Laravel, mantendo o código da aplicação desacoplado do driver.
Sumário
- Instalação
- Configuração
- Uso rápido
- Assinatura digital
- API do Serviço de PDF
- Clonagem de PDFs (MPDF)
- Compressão de PDFs
- Facade e Manager
- Contratos e Drivers
- Requisitos
- Licença
Instalação
composer require ivanilsonsousa/agnostic-pdf
O Service Provider é descoberto automaticamente pelo Laravel (auto-discovery).
Configuração
Publique o arquivo de configuração:
php artisan vendor:publish --tag=pdf-config
Isso criará config/pdf.php. Nele você define o driver principal e as opções específicas de cada driver.
Exemplo (conceitual):
return [ 'driver' => 'mpdf', // 'mpdf' (padrão) ou 'dompdf' 'mpdf' => [ // opções nativas do mPDF (ex.: 'tempDir', 'format', 'orientation', 'margin_*', etc.) ], 'dompdf' => [ // opções nativas do Dompdf (ex.: 'options' => [...], 'paper', 'orientation', etc.) ], ];
Nota: A clonagem de PDFs é um recurso do MPDF.
Configuração por chamada
config/pdf.php é a configuração da instalação. Só que formato, orientação e
margem costumam ser de cada documento: um relatório A4 com margem, uma
etiqueta de 90x29mm e um documento carimbado sem margem convivem na mesma
aplicação.
Para isso, pdf(), builder() e cloner() aceitam um array que é aplicado
por cima das opções do driver ativo:
use AgnosticPDF\Facades\PDF; // Um relatório A4 com margens próprias, sem tocar no config global: return PDF::pdf([ 'format' => 'A4', 'margin_left' => 16, 'margin_right' => 16, 'margin_top' => 26, ]) ->loadView('pdf.report', ['data' => $data]) ->streamResponse('relatorio.pdf'); // Uma etiqueta, na mesma aplicação e na mesma requisição: $etiqueta = PDF::pdf(['format' => [90, 29], 'margin_top' => 2])->loadView('pdf.label');
Só as chaves informadas mudam; as demais continuam vindo de config/pdf.php.
Cada chamada devolve uma instância nova, então uma não interfere na outra.
Por que não ajustar depois de construir? Porque não funciona: o mPDF calcula a área de escrita na construção. Um
SetMargins()posterior não reflui o conteúdo — a página continua com a largura útil antiga e o texto sangra até a borda, sem erro nenhum. Daí a configuração precisar chegar no momento em que o driver é criado.
Uso rápido
1) Renderizando uma view para resposta HTTP (controller)
use AgnosticPDF\Services\PDFService; public function showInvoice(PDFService $pdf) { $pdf->loadView('pdf.invoice', ['order' => $order]); // Retorne uma Response padrão do Laravel (sem 'exit'): return $pdf->streamResponse('invoice.pdf'); }
2) Renderizando HTML arbitrário e salvando em disco
use AgnosticPDF\Services\PDFService; public function generate(PDFService $pdf) { $html = '<h1>Olá PDF</h1><p>Gerado pela aplicação.</p>'; $pdf->loadHtml($html)->save(storage_path('app/pdfs/hello.pdf')); return 'ok'; }
3) Download direto
use AgnosticPDF\Services\PDFService; public function download(PDFService $pdf) { $pdf->loadView('pdf.report')->download('relatorio.pdf'); // Baixa o arquivo no navegador do usuário. }
Dica: para pipelines HTTP no Laravel, prefira
streamResponse()(retornaIlluminate\Http\Response).
Assinatura digital
O assinador é independente do mPDF e do Dompdf. Ele acrescenta uma assinatura
CMS/PKCS#7 destacada (adbe.pkcs7.detached) em uma revisão incremental: os
bytes que já existiam permanecem intactos. Assim, uma segunda assinatura não
reescreve nem invalida a primeira.
Consulte docs/signatures.md para a API completa, modelo de confiança, segurança, múltiplas assinaturas e limitações.
use AgnosticPDF\Drivers\PapierPdfSigner; use AgnosticPDF\Signatures\Certificate; use AgnosticPDF\Signatures\SignatureAppearance; use AgnosticPDF\Signatures\SignatureOptions; $certificate = Certificate::fromPkcs12File( '/run/secrets/signing-certificate.p12', $_ENV['PDF_CERTIFICATE_PASSWORD'], ); $appearance = SignatureAppearance::make() ->page(1) ->position(36, 24) // pontos, origem inferior esquerda ->size(280, 71) ->pdf('/path/to/vector-seal.pdf'); $signed = (new PapierPdfSigner())->sign( file_get_contents('/path/to/input.pdf'), $certificate, $appearance, SignatureOptions::make() ->signer('Maria da Silva') ->reason('Aprovação do documento'), ); file_put_contents('/path/to/signed.pdf', $signed);
pdf() usa a primeira página de um PDF como aparência vetorial, preservando o
texto. image() aceita bytes ou caminho local para PNG/JPEG. Sem aparência, a
assinatura é invisível. Também é possível usar
SignatureAppearance::default(...) para uma caixa de texto simples.
Certificado autoassinado
A biblioteca pode gerar um certificado RSA autoassinado para instalações que controlam sua própria âncora de confiança:
$certificate = Certificate::create() ->commonName('Sistema de Documentos') ->organization('Minha Organização') ->organizationalUnit('Assinatura de documentos') ->country('BR') ->validFor(1825) ->generate(); $p12 = $certificate->exportPkcs12($_ENV['PDF_CERTIFICATE_PASSWORD']);
A aplicação, não a biblioteca, deve persistir e proteger a chave privada. Um certificado autoassinado prova a integridade criptográfica, mas aparece como não confiável até que seu certificado público seja instalado ou fornecido como âncora de confiança. Ele não equivale por si só a um certificado emitido por uma autoridade certificadora ou a uma assinatura qualificada.
Verificação
use AgnosticPDF\Signatures\TrustStore; $trust = TrustStore::fromFile('/path/to/trusted-certificates.pem'); $result = (new PapierPdfSigner())->verify($signed, $trust); foreach ($result->signatures as $signature) { $signature->signerName; $signature->cryptographicallyValid; $signature->certificateTrusted; $signature->certificateValidAtSigningTime; $signature->algorithm; $signature->hasLaterChanges; $signature->certificate?->subjectName(); }
Sem TrustStore, a integridade continua sendo verificada e
certificateTrusted fica null. PDFs sem assinatura retornam um resultado
vazio. PDFs corrompidos e documentos certificados com DocMDP P=1 são
rejeitados com PdfSignatureException.
No Laravel, injete AgnosticPDF\Contracts\PdfSignerInterface ou use
PDF::signer(). Fora dele, instancie PapierPdfSigner diretamente.
Limites atuais
- assinatura CMS de aprovação compatível com ISO 32000, sem carimbo de tempo TSA, OCSP/CRL ou perfil PAdES-LT/LTA;
- suporte a restrição DocMDP
P=1; políticas FieldMDP não são interpretadas; - a verificação informa alterações posteriores, mas não classifica cada revisão posterior como permitida ou maliciosa.
API do Serviço de PDF
A interface comum aos drivers é AgnosticPDF\Contracts\PDFServiceInterface. Os métodos expostos pelo serviço principal (AgnosticPDF\Services\PDFService) espelham essa interface:
-
loadHtml(string $html): selfCarrega HTML (da página em memória) para ser renderizado pelo driver. -
loadView(string $view, array $data = []): selfRenderiza uma view do Laravel e carrega o HTML resultante. -
output(): stringRetorna o binário do PDF renderizado como string. -
download(string $filename): voidForça o download no navegador. -
save(string $path): voidSalva o PDF no caminho indicado. -
stream(string $filename): voidEnvia o PDF e finaliza a resposta.Para integração limpa com Laravel, prefira
streamResponse(). -
streamResponse(string $filename): \Illuminate\Http\ResponseRetorna umaResponsecom o PDF em inline.
Clonagem de PDFs (MPDF)
A clonagem (importar páginas de um PDF existente para o documento atual) é implementada pelo contrato AgnosticPDF\Contracts\PDFClonerDriverInterface e está disponível com o driver MPDF.
Serviço de clonagem
use AgnosticPDF\Services\PDFClonerService; public function cloneAll(PDFClonerService $cloner /* driver: MPDF */) { // Clona todas as páginas do arquivo de origem $cloner->cloneFromFile(storage_path('app/input.pdf')); // A partir daqui, as páginas clonadas estão no documento do driver em uso. // Para emitir o PDF, utilize o fluxo da sua aplicação (ver seção "Facade e Manager"). }
Assinatura (resumo) do método principal:
cloneFromFile(string $file, ?callable $callback = null, bool $force = true): self
$callback(opcional): será chamado a cada página clonada comofn(PDFClonerService $svc, int $pageNo, int $pageCount).$force(opcional): comportamento de fluxo conforme sua aplicação.
Importante: Para orquestrar clonagem + renderização no mesmo documento, utilize o Manager (abaixo), que garante que clonagem e emissão compartilham a mesma instância de driver MPDF.
Compressão de PDFs
Há um serviço de compressão baseado em processo externo (executado via Symfony\Component\Process\Process):
use AgnosticPDF\Services\PDFCompressor; public function compress(PDFCompressor $compressor) { $compressedPath = $compressor->reduce(storage_path('app/pdfs/original.pdf')); // $compressedPath aponta para o arquivo comprimido (normalmente em diretório temporário) }
Erros de compressão lançam AgnosticPDF\Exceptions\PDFCompressException, que expõe getContext(): array com detalhes úteis de depuração (comando, saída, caminho de entrada, etc.).
Facade e Manager
A facade AgnosticPDF\Facades\PDF resolve o Manager (AgnosticPDF\Services\PDFManagerService), cujos três métodos — pdf(), builder() e cloner() — aceitam configuração por chamada. Ele agrega:
- o serviço de PDF (renderização),
- o serviço de clonagem (quando disponível),
- o compressor.
O Manager oferece um builder para cenários em que você quer encadear operações (ex.: clonar páginas e em seguida renderizar/salvar) compartilhando a mesma instância de driver:
use AgnosticPDF\Facades\PDF; PDF::builder() ->addView('pdf.cover', ['title' => 'Meu PDF']) // uma capa renderizada ->addFile(storage_path('app/input.pdf')) // e o PDF existente em seguida ->save(storage_path('app/pdfs/final.pdf'));
Métodos do builder: addView, addPage, addFile, addImage, eachPage e, para emitir, save, stream e output.
O builder é útil principalmente para clonagem com MPDF seguida de emissão do PDF, assegurando que tudo ocorra no mesmo documento interno. Com o Dompdf ele funciona para pipelines que não clonam;
addFile/eachPagelançamRuntimeExceptionexplicando que a clonagem exige MPDF.
Contratos e Drivers
Contratos
-
AgnosticPDF\Contracts\PDFServiceInterfaceOperações de renderização/saída:loadHtml,loadView,output,download,save,stream,streamResponse,addPage,getEngineetap. -
AgnosticPDF\Contracts\PDFClonerDriverInterface(MPDF) Clonagem de páginas:prepareClone(string $pathFile): int→ retorna o número de páginas do PDF origem;clonePage(int $pageNo): void→ importa a página para o documento atual.
-
AgnosticPDF\Contracts\PdfSignerInterfaceAssinatura incremental e verificação de assinaturas existentes, sem depender do driver de renderização ou do Laravel.
Drivers disponíveis
-
AgnosticPDF\Drivers\MPDFDriverImplementaPDFServiceInterfaceePDFClonerDriverInterface. FornecegetEngine(): \Mpdf\Mpdfpara configurações avançadas do mPDF — outap(callable), que entrega a mesma instância a um callback. -
AgnosticPDF\Drivers\DompdfDriverImplementaPDFServiceInterface. Por padrão, habilita recursos remotos e usaA4 portrait.
Você seleciona o driver ativo via
config('pdf.driver').
Requisitos
- PHP
^8.2(Laravel 13 requer PHP 8.3 ou superior) - extensões
openssl,mbstringezlib - Laravel
^12.0 || ^13.0somente para provider, facade, views e respostas HTTP - Drivers:
mpdf/mpdf:^8.2dompdf/dompdf:^3.1papier/papier:^3.0
Namespace & Provider
As classes públicas estão sob AgnosticPDF\... e o Service Provider é AgnosticPDF\AgnosticPDFServiceProvider (auto-discovery via composer.json).
Exemplos rápidos (copie-e-cole)
Controller – stream inline
use AgnosticPDF\Services\PDFService; public function show(PDFService $pdf) { return $pdf->loadView('pdf.ticket', ['ticket' => $ticket]) ->streamResponse('ticket.pdf'); }
Salvar em disco
use AgnosticPDF\Services\PDFService; $pdf->loadHtml('<h1>Relatório</h1>')->save(storage_path('app/pdfs/relatorio.pdf'));
Compressão
use AgnosticPDF\Services\PDFCompressor; $compressed = app(PDFCompressor::class)->reduce(storage_path('app/pdfs/relatorio.pdf'));
Clonagem (MPDF) + emissão via Builder
use AgnosticPDF\Facades\PDF; PDF::builder() ->addFile(storage_path('app/pdfs/base.pdf')) ->addView('pdf.appendix', ['data' => $data]) ->save(storage_path('app/pdfs/final.pdf'));