HTML para PDF fiel ao navegador, sem navegador no servidor. O navegador de quem exporta calcula o layout; o PHP só desenha o PDF. Funciona em hospedagem compartilhada (PHP + GD).
Ver a galeria de exemplosFormato do JSON (format.md)Licença MITVisão geral
Gerar PDF a partir de HTML costuma cair num destes becos:
| Opção | Problema |
|---|---|
| Chrome / Puppeteer no servidor | precisa de VPS ou container; não roda em hospedagem compartilhada |
| wkhtmltopdf | binário no servidor, CSS antigo, projeto descontinuado |
| Dompdf / mPDF / TCPDF lendo o HTML | entendem só CSS 2.1: sem flexbox, grid, variáveis, clamp(), filtros… o PDF sai diferente da tela |
| API externa de PDF | custo, dependência de terceiros e o conteúdo sai do seu servidor |
O Jh2Pdf parte de outra ideia: o navegador de quem exporta já é o melhor motor de layout que existe, e já está aberto. Em vez de reimplementar CSS no PHP, o navegador calcula onde fica cada palavra, caixa, borda e imagem, e o PHP só desenha essas posições. Não há quebra de linha nem cálculo de layout no servidor para errar.
calc(): tudo o que o navegador entende, porque é ele quem mede.Como funciona
NAVEGADOR (js/jh2pdf.js) SERVIDOR (PHP)
──────────────────────── ──────────────
seu HTML + seu CSS Options (tempDir, fontDir, imageResolver…)
│ desenha cada página fora da tela │
│ (espera fontes e imagens) ▼
▼ Layout::clean() valida tudo (o JSON é hostil)
extractLayout() percorre o DOM: │
getComputedStyle + Range.getClientRects por palavra ▼
│ Renderer::render() → Dompdf\Cpdf
▼ R retângulo · B bordas · T texto · I imagem
JSON de operações ──────── POST ────────────────────► M marca à mão · L linha · C/E recorte
{ w, h, pages: [{ bg, ops: [...] }] } │
▼
bytes do PDF
- Medição (navegador). Cada página é inserida num contêiner fora da tela, dentro do seu próprio documento, então o seu CSS vale.
O módulo espera as fontes (
document.fonts.ready) e as imagens (até 4 s cada). - Extração. Para cada elemento visível, lê o estilo calculado e as caixas; para cada palavra, a posição e a largura exatas. O resultado é uma lista ordenada de operações de desenho (a ordem é a ordem de pintura).
- Desenho (servidor). O PHP valida o JSON e desenha as operações com o
Cpdfdo Dompdf, usado só como "canvas" de PDF. Cada palavra é posicionada na linha de base e tem o espaçamento entre letras ajustado para ter exatamente a largura medida.
Requisitos
| Onde | Requisito | Observação |
|---|---|---|
| Navegador | qualquer navegador moderno (Chromium, Firefox, Safari) | usa módulos ES, Range.getClientRects, document.fonts |
| Servidor | PHP ≥ 8.1 | testado em 8.2 |
| Servidor | extensões gd e mbstring | GD: imagens, filtros e sombras · mbstring: texto UTF-8 |
| Servidor | dompdf/dompdf ^3.1 | instalado pelo Composer (veja Dependências) |
| Servidor | uma pasta gravável | cache de fontes e imagens temporárias (Options::$tempDir) |
Instalação
Com o Composer
composer require geisonq/jh2pdf
Ou, a partir deste repositório: composer install na raiz (instala o Dompdf em vendor/).
Sem o Composer
Copie a pasta e use o autoloader incluído. O Dompdf continua necessário e precisa ser carregado por você:
require '/caminho/jh2pdf/autoload.php'; // classes Jh2Pdf\*
require '/caminho/vendor/autoload.php'; // Dompdf
No navegador
Copie js/jh2pdf.js para o seu site. É um módulo ES sem dependências (~30 KB, sem minificar).
Início rápido
1. Navegador
import { extractLayout } from './jh2pdf.js';
const layout = await extractLayout(
['<h1>Olá</h1><p>Primeira página</p>', '<p>Segunda página</p>'], // um fragmento de HTML por página
{ width: 794, height: 1123 } // A4 retrato em px CSS
);
const res = await fetch('/pdf.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(layout),
});
const pdf = await res.blob(); // application/pdf
window.open(URL.createObjectURL(pdf));
2. Servidor (pdf.php)
require 'vendor/autoload.php';
$raw = file_get_contents('php://input', false, null, 0, 5_000_000); // limite o corpo
$layout = json_decode((string)$raw, true);
if (!is_array($layout)) { http_response_code(400); exit; }
$options = new Jh2Pdf\Options('/caminho/gravavel');
try {
$pdf = (new Jh2Pdf\Renderer($options))->render($layout, 'Título do documento');
} catch (Jh2Pdf\Exception $e) {
error_log((string)$e->getPrevious()); // o detalhe fica na exceção anterior
http_response_code(500); exit;
}
header('Content-Type: application/pdf');
echo $pdf;
Exemplos executáveis: examples/minimal/ (o mínimo) e demo/ (galeria com tabelas, efeitos e imagens).
API JavaScript
extractLayout(pages, options) → Promise<Layout>
pages é um array com uma entrada por página: uma string de HTML, um objeto { html } ou qualquer valor, se você passar wrap.
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
width, height | número (px) | — | Obrigatórios. Tamanho da página em px CSS. No PDF vira pt × 0,75 (794×1123 px = A4). |
wrap(page, i) | função → string | <div>…</div> | HTML completo da página, com um único elemento raiz. O background-color da raiz vira o fundo da página. Útil para passar elementos já existentes: wrap: (el) => el.outerHTML. |
hostCss | string | — | CSS extra dentro do contêiner de medição (por exemplo, um reset). Escope com [data-jh2pdf-host]. |
marks | boolean | false | Desenha as marcas à mão (.mk.mk-circle etc.). |
skip(el) | função → boolean | — | true para não desenhar um elemento e o que há dentro dele. |
acceptImage(src) | função → boolean | — | Filtra os src de imagem enviados. Quem decide de verdade é o servidor. |
sanitize(html) | função → string | — | Sanitizador do HTML de cada página (por exemplo, DOMPurify). O filtro interno roda por cima. Veja Segurança. |
Retorna { w, h, pages: [{ bg, ops }] }, pronto para JSON.stringify. Lança Error se faltar width ou height.
.page) e não depender do contêiner em volta.
O contêiner fica em position: fixed, fora da tela, e herda as propriedades do <body>. Declare fonte, cor e line-height na raiz da página.API PHP
Namespace Jh2Pdf, PHP 8.1 com strict_types.
| Classe | Papel |
|---|---|
Renderer | __construct(Options $o) · render(array $layout, string $title = ''): string. Devolve os bytes do PDF e lança Jh2Pdf\Exception se não conseguir · missingFonts() lista as fontes do último PDF que o servidor não tinha. |
Options | Configuração. Só tempDir é obrigatório (veja Options). |
ImageResolver | Interface load(string $src): ?string. Você decide de onde vêm as imagens e devolve null para recusar. |
DataUriImageResolver | Resolvedor padrão. Só aceita data:image/(png|jpeg|gif|webp);base64,…, de até 12 MB (__construct(int $maxBytes)). |
Exception | Estende RuntimeException. A mensagem é genérica, sem caminhos do servidor; o detalhe fica em getPrevious(). |
Layout | clean(array, ?Options) valida o JSON · num($v, $min, $max) e color($v) são conversores seguros · OPS = 'RBTILMCE'. |
ImageFilter | apply(string $dataUri, string $filter, …): ?string aplica filtros CSS com a GD. Pode ser usada sozinha. |
Images | Interna: resolve, filtra, reduz (shrink) e gera sombras (shadow). dimensionsOk() valida o cabeçalho antes de decodificar. |
FontFaces | css(Options $o, string|callable $url = '', string $display = 'block'): string gera o CSS @font-face do navegador a partir das mesmas fontes que o PDF usa. |
FontIndex | Interna: a lista de fontes que o servidor tem (addFont + pastas), a fonte única para o PDF e para o FontFaces. |
Fonts | Interna: escolhe o arquivo por família, estilo e peso (regras do CSS) e o registra no Dompdf; itálico simulado se a família não tiver. |
Marks, Svg | Internas: leem os desenhos das marcas (SVG dentro do CSS) e achatam os caminhos SVG em polígonos. |
Options
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
tempDir | string | — | Obrigatório (1º argumento do construtor). Pasta gravável: cache de fontes do Dompdf e imagens temporárias. |
fontDir | string | fonts/ da lib | Pasta de TTF estáticos nomeados <familia>-<peso>[-italic].ttf (2º argumento do construtor). |
addFont($family, $weight, $file, $italic) | method | — | Registra um TTF estático para uma família, um peso (100–900) e um estilo, com qualquer nome de arquivo. Vale mais que as pastas. Devolve $this. |
addFontDir($dir) | method | — | Outra pasta de TTF nomeados <familia>-<peso>[-italic].ttf. Devolve $this. |
imageResolver | ?ImageResolver | DataUriImageResolver | De onde vêm as imagens. |
marksCss | ?string | resources/marks.css | Arquivo CSS com os desenhos das marcas à mão. |
creator | string | 'Jh2Pdf' | Metadado "Creator" do PDF. |
fallbackFont | string | 'DejaVu Sans' | Fonte usada quando nenhuma família da lista font-family existe no servidor. Veja strictFonts. |
strictFonts | bool | false | Lança Jh2Pdf\Exception (Missing fonts: Roboto 400.) antes de desenhar, em vez de usar a fallbackFont em silêncio. Recomendado em desenvolvimento. |
onMissingFont | ?callable | null | Chamado uma vez por fonte que faltou, com ($family, $weight, $italic), antes de desenhar. Use para registrar no log. |
maxPages | int | 1000 | Páginas além disso são descartadas. |
maxOpsPerPage | int | 60 000 | Operações por página. |
maxOpsTotal | int | 400 000 | Operações no documento inteiro. |
maxImages | int | 200 | Imagens processadas por PDF (cada uma custa CPU e memória). |
memoryLimit | ?string | '512M' | Sobe o memory_limit durante a geração (nunca baixa). null = não mexer. |
pxToPt | float | 0.75 | Conversão de px CSS para pt (96 dpi). |
Formato do JSON
É o contrato entre o JS e o PHP. Medidas em px CSS, origem no canto superior esquerdo, cores #rrggbb, opacidade de 0 a 1.
A referência completa está em docs/format.md.
{ "w": 794, "h": 1123,
"pages": [ { "bg": "#ffffff", "ops": [
{ "t": "R", "x": 0, "y": 0, "w": 794, "h": 300, "c": "#4f46e5", "a": 1, "r": [12,12,0,0] },
{ "t": "T", "x": 60, "y": 80, "w": 92.4, "h": 44, "s": "Lorem", "f": "Jh Display", "g": 700, "z": 40, "c": "#ffffff" }
] } ] }
| Op | Nome | Campos principais |
|---|---|---|
R | retângulo cheio | x y w h c a r[4] |
B | bordas | x y w h s[4]=[largura,cor,a] (topo, direita, base, esquerda) r[4] |
T | texto (uma palavra) | x y w h s f fb g i z c a l d (fb: famílias de reserva da lista font-family; g: peso 100–900; d: 1 sublinhado, 2 riscado) |
I | imagem | x y w h u k[4] f a r[4] (u = src, k = recorte, f = filtro CSS) |
M | marca à mão | m v x y w h c z |
L | linha | x y x2 y2 c w a |
C / E | início / fim de recorte arredondado | x y w h r[4] (aninháveis até 32 níveis) |
CSS suportado
O layout em si (flexbox, grid, posicionamento, calc(), variáveis, unidades) é sempre suportado, porque quem calcula é o navegador.
A tabela abaixo trata do que é desenhado.
| Recurso | Situação | Detalhes |
|---|---|---|
| Texto: família (com reservas), peso 100–900, itálico, tamanho, cor, opacidade | Sim | Usa o peso mais próximo que você tiver, como o CSS. Família sem itálico é inclinada em 12°, como faz o navegador. Precisa de um TTF estático no servidor: veja Fontes. |
text-transform, letter-spacing | Sim | A largura final é sempre a medida pelo navegador. |
text-decoration (sublinhado, riscado) | Parcial | A linha é desenhada palavra por palavra (não passa sob os espaços). Cor e estilo da decoração são ignorados. |
background-color, border-radius (por canto) | Sim | |
linear-gradient | Parcial | Vertical e horizontal, com várias paradas, desenhado como faixas finas. Ângulos diagonais viram a direção mais próxima. Sem radial, repeating ou camadas. |
| Bordas (largura e cor por lado) | Parcial | dashed, dotted e double saem sólidas. Bordas muito grossas e arredondadas podem mostrar emendas finas. |
box-shadow sem desfoque (externa e interna) | Sim | Várias sombras e spread. Interna: só deslocamento, sem spread. |
box-shadow com desfoque, text-shadow | Não | São ignoradas. |
opacity | Sim | Multiplicada pelos ancestrais. |
overflow: hidden com border-radius | Sim | Recorte arredondado de tudo o que estiver dentro. |
<img> PNG, JPEG, GIF, WebP | Sim | Com object-fit (cover, contain, none, scale-down) e cantos arredondados. |
filter em imagens | Sim | grayscale sepia brightness contrast invert blur saturate hue-rotate opacity drop-shadow (aplicados com a GD). |
Listas: disc circle square decimal lower/upper-alpha, ::marker | Sim | Inclusive start e value. |
::before/::after vazios (content: '') | Parcial | Com fundo, borda, cantos e sombra sólida, quando position: absolute ou display: block. Não funciona em inline-block. |
Pseudo-elementos com texto, contadores, ::first-letter | Não | Use um elemento de verdade (<span>) para números e rótulos. |
background-image: url(), transform, clip-path, mask | Não | |
SVG inline, <canvas>, <video>, <iframe> | Não | Converta o canvas em <img src="data:…"> antes de extrair (a demo faz isso). |
| Ligaduras de fonte | Não | Por exemplo, => na Fira Code vira dois caracteres. |
| Links clicáveis no PDF | Não | Nenhuma anotação ou ação é criada (é uma decisão de segurança). |
Páginas e tamanhos
Cada item de pages é uma página do PDF, do tamanho de width × height. Não há paginação automática:
o que passar da altura da página é cortado. Divida o conteúdo em páginas antes de extrair (por exemplo, uma seção por página, ou medindo alturas no navegador).
| Formato | px CSS (width × height) | PDF (pt) |
|---|---|---|
| A4 retrato | 794 × 1123 | 595 × 842 |
| A4 paisagem | 1123 × 794 | 842 × 595 |
| Carta (Letter) | 816 × 1056 | 612 × 792 |
| Slide 16:9 | 1280 × 720 | 960 × 540 |
O tamanho aceito vai de 200 a 4000 px por lado.
Fontes
Para o PDF sair igual à tela, o navegador e o servidor precisam usar os mesmos arquivos de fonte. O Dompdf não lê woff2 nem fontes variáveis, então o servidor usa TTF estáticos, um por peso. Há três jeitos de dar fontes a ele:
| Jeito | Quando usar |
|---|---|
pasta fonts/ (Options::$fontDir) | arquivos nomeados <familia>-<peso>[-italic].ttf: dm-sans-600.ttf, dm-sans-400-italic.ttf (família em minúsculas com hífens, peso de 100 a 900) |
$options->addFontDir($dir) | outra pasta com a mesma convenção de nomes; use para as suas fontes e mantenha as incluídas |
$options->addFont($family, $weight, $file, $italic) | um arquivo com qualquer nome; vale mais que as pastas |
$options = new Jh2Pdf\Options('/caminho/gravavel');
$options->addFont('Roboto', 400, '/fonts/Roboto-Regular.ttf')
->addFont('Roboto', 400, '/fonts/Roboto-Italic.ttf', italic: true)
->addFont('Roboto', 700, '/fonts/Roboto-Bold.ttf')
->addFontDir('/mais/fontes');
Como a fonte é escolhida
As mesmas regras do CSS:
- Família: a primeira família da lista
font-familyque o servidor tem ('Roboto', Arial, sans-serifusa Roboto, ou Arial se você a tiver; nomes genéricos comosans-serifnão são fontes). O navegador envia até 5 reservas depois da primeira. - Estilo: itálico, se a família tem arquivo itálico; senão, o estilo normal inclinado em 12°, como o navegador faz.
- Peso: de 100 a 900, o arquivo mais próximo que você tiver: pedir 800 com só 400 e 700 usa o 700; 300 usa o 400.
Faça o navegador usar os mesmos arquivos
Gere o CSS @font-face a partir das mesmas fontes, para nunca haver duas listas para manter iguais:
// uma vez, ao publicar (ler os nomes dentro dos TTF é lento demais para fazer a cada requisição)
file_put_contents('public/fonts.css', Jh2Pdf\FontFaces::css($options, '/fonts/'));
ou pela linha de comando: php tools/fonts-css.php --dir fonts --url /fonts/ --out public/fonts.css Instalado pelo Composer, o script fica em vendor/geisonq/jh2pdf/tools/fonts-css.php.
Nunca falhe em silêncio
Uma família que o servidor não tem sai na fallbackFont (DejaVu Sans), e o PDF fica diferente da tela. Para pegar isso:
$options->strictFonts = true; // lança Jh2Pdf\Exception: "Missing fonts: Roboto 400, Roboto 700 italic."
$options->onMissingFont = fn(string $family, int $weight, bool $italic) => error_log("Fonte faltando: $family $weight");
$pdf = $renderer->render($layout);
$renderer->missingFonts(); // [['family' => 'Roboto', 'weight' => 400, 'italic' => false], …]
A checagem acontece antes de desenhar qualquer coisa. Use strictFonts em desenvolvimento e nos testes; em produção, registre no log com onMissingFont ou missingFonts().
Adicionar uma fonte a partir de uma fonte variável
pip install fonttools brotli
python tools/generate-fonts.py --fonts fonts.json --src pasta-das-fontes --out fonts
Python só é necessário para gerar os arquivos (--weights 300,400,700 escolhe os pesos; o formato do fonts.json está no cabeçalho do script). Ele grava a família, o estilo e o peso certos dentro de cada TTF. Para corrigir os nomes de TTF estáticos que você já tem: python tools/generate-fonts.py --fix-names pasta. O servidor usa os TTF prontos. Confira se a licença da fonte permite embuti-la em PDF.
Incluídas (12 famílias, SIL OFL 1.1): Barlow Condensed, DM Sans, Fira Code, Inter, JetBrains Mono, Montserrat, Nunito, Open Sans e quatro serifadas renomeadas: Jh Serif (da Lora), Jh Serif Text (da Merriweather), Jh Display (da Playfair Display) e Jh Serif Book (da Source Serif 4). Têm itálico: DM Sans e Jh Serif.
Imagens
O src de cada imagem vem do navegador e não é confiável. Por padrão só data: é aceito (nada de disco nem de rede).
Para servir imagens do seu sistema, implemente um resolvedor e valide se aquele usuário pode ver aquele arquivo:
final class MeuResolver implements Jh2Pdf\ImageResolver
{
public function load(string $src): ?string
{
// só o seu padrão de URL; nunca concatene o src num caminho
if (!preg_match('~^/uploads/([a-f0-9]{16}\.(png|jpg))$~', $src, $m)) return null;
$f = '/var/app/uploads/' . $m[1];
return is_file($f) ? file_get_contents($f) : null;
}
}
$options->imageResolver = new MeuResolver();
No navegador, acceptImage evita enviar o que o servidor recusaria. As imagens são reduzidas ao tamanho em que aparecem (~150 dpi):
sem transparência viram JPEG, com transparência continuam PNG.
Marcas à mão
Extensão opcional, desligada por padrão (não é HTML/CSS padrão), para destacar trechos com um traço "feito à mão": underline, circle, highlight, box, strike, cross.
<span class="mk mk-circle" style="--mk-color:#e5484d">importante</span>
Com marks: true, o extrator envia a caixa do trecho e a cor; o PHP desenha o SVG da forma, que está em resources/marks.css
(ou no seu CSS, via Options::$marksCss). Sem --mk-color, usa a cor do texto. Os elementos pares usam a 2ª variação (:nth-of-type(even)), e uma marca com display: inline
que quebra de linha ganha um desenho por linha. Sem marks: true, as classes .mk são tratadas como qualquer outro elemento.
O CSS de tela (máscara no ::after) está em demo/pages.css.
Segurança
Há duas fronteiras, e em cada uma o que vem do outro lado é tratado como hostil.
Servidor: o JSON do navegador
Qualquer pessoa que consiga chamar o seu endpoint pode mandar um JSON feito à mão. Por desenho, o servidor nunca interpreta HTML, CSS ou JavaScript.
| Ataque | Defesa |
|---|---|
| Injetar HTML, JS ou CSS no texto | O texto vai para o PDF como glifos (em hexadecimal) e nunca é interpretado. |
Injetar objetos do PDF (/JS, /OpenAction, /URI, /Launch) | Texto em hexadecimal, título escapado e nenhum objeto de ação ou anotação criado. Há teste para isso. |
Nome de fonte com ../, ) ou byte nulo | Reduzido a [A-Za-z0-9 -] (60 caracteres; no máximo 5 famílias de reserva) e procurado só entre as fontes que você registrou. Os caminhos vêm da sua configuração, nunca do JSON. |
Ler arquivos locais ou fazer requisições (file://, http://, ../) | Imagens só passam pelo ImageResolver. No Dompdf ficam desligados isRemoteEnabled, PHP e JS, e há chroot. |
| Imagem-bomba (PNG pequeno que declara milhões de pixels) | O cabeçalho é lido antes de decodificar: lado ≤ 20 000 px e ≤ 25 megapixels. |
| Esgotar CPU ou memória | Limites de páginas, operações, imagens e recortes aninhados (≤ 32), tetos para sombras e desfoques, set_time_limit(180). |
Números ou tipos absurdos (1e999, NaN, arrays, url(...)) | Layout::num() limita a um intervalo e Layout::color() só aceita #rgb/#rrggbb. |
| Vazamento de caminhos em mensagens de erro | Só é lançada Jh2Pdf\Exception com mensagem genérica. |
| Arquivos temporários esquecidos | Nomes aleatórios, apagados ao fim de cada PDF e varridos depois de 1 hora. |
ImageResolver quando ele lê arquivos. Mantenha o Dompdf, o PHP e a GD atualizados.Navegador: o HTML de entrada
O HTML de cada página entra no DOM de quem está exportando. Se ele vier de outra pessoa, é uma entrada hostil (XSS). O módulo primeiro coloca o HTML num
<template> inerte e remove script, iframe, object, embed, link, meta,
base, atributos on*, srcdoc, URLs javascript: e data: que não sejam imagem, e expression().
Isso é um filtro de reserva: para conteúdo de terceiros, use um sanitizador de verdade:
extractLayout(pages, { width, height, sanitize: (h) => DOMPurify.sanitize(h) });
<style> e url() no CSS continuam permitidos. Eles não executam código, mas podem disparar requisições (rastreamento).
Desempenho
Medido com a galeria de exemplos (7 páginas A4, ~2 400 operações, 9 imagens com filtro) num PC comum com XAMPP e PHP 8.2:
| Etapa | Resultado |
|---|---|
| JSON enviado | ~460 KB (a maior parte é a imagem data: repetida) |
| PDF gerado | ~300 KB, fontes embutidas como subconjunto |
| PHP, primeira geração | ~6–9 s (registra as fontes no cache do Dompdf) |
| PHP, gerações seguintes | ~2–3 s, dominados pelos filtros de imagem na GD |
- Os filtros (
blur,drop-shadow) são a parte mais cara. Sem eles, páginas de texto e tabelas levam frações de segundo. - A mesma imagem com o mesmo filtro e tamanho é processada uma vez só por PDF (há cache).
- Para documentos grandes, envie as imagens por URL e use um
ImageResolver, em vez dedata:repetido no JSON.
Solução de problemas
| Sintoma | Causa provável e solução |
|---|---|
| Texto com fonte diferente da tela | O servidor não tem TTF estático para nenhuma família da lista font-family e usou a fallbackFont. Adicione a fonte (veja Fontes); strictFonts ou missingFonts() mostram quais faltam. |
| Palavras espremidas ou espaçadas demais | O navegador mediu com outra fonte (a de reserva ou a do sistema). Carregue os mesmos arquivos com @font-face: gere o CSS com FontFaces::css(). |
| Imagem não aparece | O src não é data: e não há ImageResolver, ou o resolvedor devolveu null, ou a imagem passou dos limites. |
| Conteúdo cortado no fim da página | Não há paginação automática. Divida em mais páginas. |
| Estilos da sua aplicação "vazam" para a página | O contêiner herda do <body>. Escope o CSS da página e defina fonte, cor e line-height na raiz. |
Dompdf is not installed | Rode composer install ou carregue o vendor/autoload.php antes. |
Options::$tempDir must be an existing writable directory | Crie a pasta e dê permissão de escrita ao usuário do PHP. |
| Erro 500 sem detalhe | É de propósito. Registre $e->getPrevious() no seu log. |
Dependências
Em produção
| Pacote | Versão | Licença | Para que serve |
|---|---|---|---|
| dompdf/dompdf | ^3.1 (testado 3.1.6) | LGPL-2.1 | Só o Cpdf (escrita do PDF), as métricas e o registro de fontes. O parser de HTML/CSS do Dompdf não é usado. |
| dompdf/php-font-lib | 1.0.2 | LGPL-2.1-or-later | Lê os TTF e gera o subconjunto embutido no PDF. |
| dompdf/php-svg-lib | 1.0.2 | LGPL-3.0-or-later | Dependência do Dompdf (não usada diretamente). |
| masterminds/html5 | 2.11.0 | MIT | Dependência do Dompdf (não usada diretamente). |
| sabberworm/php-css-parser | 9.5.0 | MIT | Dependência do Dompdf (não usada diretamente). |
Extensão gd | — | PHP | Decodificação, redução, filtros e sombras de imagens. |
Extensão mbstring | — | PHP | Texto UTF-8. |
Fontes em fonts/ | — | SIL OFL 1.1 | Embutidas nos PDFs. Avisos em fonts/OFL.txt. |
No navegador: nenhuma dependência (js/jh2pdf.js é JavaScript puro).
Só em desenvolvimento
| Ferramenta | Licença | Para que serve |
|---|---|---|
| Composer | MIT | Instalar o Dompdf e rodar composer test. |
Python 3 + fonttools + brotli | MIT | tools/generate-fonts.py: gerar TTF estáticos a partir de fontes variáveis. |
fonts/OFL.txt.
Se gerar instâncias novas de uma fonte com RFN, renomeie-as também.Licença
O código do Jh2Pdf está sob a licença MIT, © 2026 Geison Quevedo. Qualquer pessoa pode usar, copiar, modificar, mesclar, publicar, distribuir, sublicenciar e vender, inclusive em projetos fechados, desde que mantenha o aviso de copyright e o texto da licença. As fontes (OFL) e o Dompdf (LGPL) têm as licenças próprias, descritas em Dependências.
Estrutura
jh2pdf/
├── LICENSE MIT
├── README.md resumo e início rápido
├── composer.json pacote (autoload PSR-4: Jh2Pdf\ → src/)
├── autoload.php autoloader para quem não usa o Composer
├── src/ PHP: Renderer, Options, Layout, ImageResolver, DataUriImageResolver,
│ Images, ImageFilter, Fonts, Marks, Svg, Exception
├── js/jh2pdf.js extrator de layout (módulo ES, sem dependências)
├── fonts/ TTF estáticos (OFL) + OFL.txt
├── resources/marks.css desenhos SVG das marcas à mão
├── docs/ esta documentação (en, pt-BR, es) + format.md (o formato do JSON)
├── demo/ galeria de exemplos (index.html, pages.css, fonts.css, pdf.php)
├── examples/minimal/ o menor exemplo completo
├── tests/run.php testes (php tests/run.php ou composer test)
├── tools/fonts-css.php gera o CSS @font-face do navegador a partir das mesmas fontes
└── tools/generate-fonts.py gera os TTF a partir de fontes variáveis
Contribuindo
- Testes:
composer installecomposer test. Não há framework de testes, sótests/run.php. - Critério de qualidade: ficar igual à tela. Gere o PDF pelo navegador (Imprimir → Salvar como PDF) e por esta biblioteca e compare
lado a lado, ampliando (por exemplo, com PyMuPDF:
page.get_pixmap(dpi=200)). - Nova operação (por exemplo, degradê radial): (1) o extrator em
js/jh2pdf.jspassa a emitir a operação; (2)Layout::OPSganha a letra; (3)Renderer::renderganha ocase; (4) documente emdocs/format.md; (5) teste emtests/run.php. Valide todo campo novo: é entrada hostil. - Estilo: PHP 8.1 com
declare(strict_types=1), sem dependências além do Dompdf. JS sem dependências, com nomes de topo prefixados compl. - Contribuições são aceitas sob a mesma licença do projeto (MIT).
