Jh2pdf — JavaScript HTML 2 PDF

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 MIT

Visão geral

Gerar PDF a partir de HTML costuma cair num destes becos:

OpçãoProblema
Chrome / Puppeteer no servidorprecisa de VPS ou container; não roda em hospedagem compartilhada
wkhtmltopdfbinário no servidor, CSS antigo, projeto descontinuado
Dompdf / mPDF / TCPDF lendo o HTMLentendem só CSS 2.1: sem flexbox, grid, variáveis, clamp(), filtros… o PDF sai diferente da tela
API externa de PDFcusto, 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.

Fiel à telaflexbox, grid, variáveis CSS, calc(): tudo o que o navegador entende, porque é ele quem mede.
Texto de verdadetexto selecionável e pesquisável, com as fontes embutidas (subconjunto).
Hospedagem simplessó PHP 8.1 + GD + mbstring. Sem Chrome, sem Node, sem binários.
Seguro por desenhoo servidor nunca interpreta HTML, CSS ou JS: só números, cores e texto validados.

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
  1. 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).
  2. 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).
  3. Desenho (servidor). O PHP valida o JSON e desenha as operações com o Cpdf do 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

OndeRequisitoObservação
Navegadorqualquer navegador moderno (Chromium, Firefox, Safari)usa módulos ES, Range.getClientRects, document.fonts
ServidorPHP ≥ 8.1testado em 8.2
Servidorextensões gd e mbstringGD: imagens, filtros e sombras · mbstring: texto UTF-8
Servidordompdf/dompdf ^3.1instalado pelo Composer (veja Dependências)
Servidoruma pasta gravávelcache 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çãoTipoPadrãoDescrição
width, heightnú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.
hostCssstring—CSS extra dentro do contêiner de medição (por exemplo, um reset). Escope com [data-jh2pdf-host].
marksbooleanfalseDesenha 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.

Dica: como a medição acontece no seu documento, o CSS da página deve ser escopado numa classe (por exemplo, .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.

ClassePapel
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.
OptionsConfiguração. Só tempDir é obrigatório (veja Options).
ImageResolverInterface load(string $src): ?string. Você decide de onde vêm as imagens e devolve null para recusar.
DataUriImageResolverResolvedor padrão. Só aceita data:image/(png|jpeg|gif|webp);base64,…, de até 12 MB (__construct(int $maxBytes)).
ExceptionEstende RuntimeException. A mensagem é genérica, sem caminhos do servidor; o detalhe fica em getPrevious().
Layoutclean(array, ?Options) valida o JSON · num($v, $min, $max) e color($v) são conversores seguros · OPS = 'RBTILMCE'.
ImageFilterapply(string $dataUri, string $filter, …): ?string aplica filtros CSS com a GD. Pode ser usada sozinha.
ImagesInterna: resolve, filtra, reduz (shrink) e gera sombras (shadow). dimensionsOk() valida o cabeçalho antes de decodificar.
FontFacescss(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.
FontIndexInterna: a lista de fontes que o servidor tem (addFont + pastas), a fonte única para o PDF e para o FontFaces.
FontsInterna: 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, SvgInternas: leem os desenhos das marcas (SVG dentro do CSS) e achatam os caminhos SVG em polígonos.

Options

PropriedadeTipoPadrãoDescrição
tempDirstring—Obrigatório (1º argumento do construtor). Pasta gravável: cache de fontes do Dompdf e imagens temporárias.
fontDirstringfonts/ da libPasta 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?ImageResolverDataUriImageResolverDe onde vêm as imagens.
marksCss?stringresources/marks.cssArquivo CSS com os desenhos das marcas à mão.
creatorstring'Jh2Pdf'Metadado "Creator" do PDF.
fallbackFontstring'DejaVu Sans'Fonte usada quando nenhuma família da lista font-family existe no servidor. Veja strictFonts.
strictFontsboolfalseLança Jh2Pdf\Exception (Missing fonts: Roboto 400.) antes de desenhar, em vez de usar a fallbackFont em silêncio. Recomendado em desenvolvimento.
onMissingFont?callablenullChamado uma vez por fonte que faltou, com ($family, $weight, $italic), antes de desenhar. Use para registrar no log.
maxPagesint1000Páginas além disso são descartadas.
maxOpsPerPageint60 000Operações por página.
maxOpsTotalint400 000Operações no documento inteiro.
maxImagesint200Imagens 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.
pxToPtfloat0.75Conversã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" }
  ] } ] }
OpNomeCampos principais
Rretângulo cheiox y w h c a r[4]
Bbordasx y w h s[4]=[largura,cor,a] (topo, direita, base, esquerda) r[4]
Ttexto (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)
Iimagemx y w h u k[4] f a r[4] (u = src, k = recorte, f = filtro CSS)
Mmarca à mãom v x y w h c z
Llinhax y x2 y2 c w a
C / Einício / fim de recorte arredondadox 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.

RecursoSituaçãoDetalhes
Texto: família (com reservas), peso 100–900, itálico, tamanho, cor, opacidadeSimUsa 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-spacingSimA largura final é sempre a medida pelo navegador.
text-decoration (sublinhado, riscado)ParcialA 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-gradientParcialVertical 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)Parcialdashed, dotted e double saem sólidas. Bordas muito grossas e arredondadas podem mostrar emendas finas.
box-shadow sem desfoque (externa e interna)SimVárias sombras e spread. Interna: só deslocamento, sem spread.
box-shadow com desfoque, text-shadowNãoSão ignoradas.
opacitySimMultiplicada pelos ancestrais.
overflow: hidden com border-radiusSimRecorte arredondado de tudo o que estiver dentro.
<img> PNG, JPEG, GIF, WebPSimCom object-fit (cover, contain, none, scale-down) e cantos arredondados.
filter em imagensSimgrayscale sepia brightness contrast invert blur saturate hue-rotate opacity drop-shadow (aplicados com a GD).
Listas: disc circle square decimal lower/upper-alpha, ::markerSimInclusive start e value.
::before/::after vazios (content: '')ParcialCom 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-letterNãoUse um elemento de verdade (<span>) para números e rótulos.
background-image: url(), transform, clip-path, maskNão
SVG inline, <canvas>, <video>, <iframe>NãoConverta o canvas em <img src="data:…"> antes de extrair (a demo faz isso).
Ligaduras de fonteNãoPor exemplo, => na Fira Code vira dois caracteres.
Links clicáveis no PDFNãoNenhuma 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).

Formatopx CSS (width × height)PDF (pt)
A4 retrato794 × 1123595 × 842
A4 paisagem1123 × 794842 × 595
Carta (Letter)816 × 1056612 × 792
Slide 16:91280 × 720960 × 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:

JeitoQuando 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:

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.

AtaqueDefesa
Injetar HTML, JS ou CSS no textoO 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 nuloReduzido 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óriaLimites 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 erroSó é lançada Jh2Pdf\Exception com mensagem genérica.
Arquivos temporários esquecidosNomes aleatórios, apagados ao fim de cada PDF e varridos depois de 1 hora.
O que fica por sua conta: autenticação, CSRF, rate limit e limite do tamanho do corpo da requisição (gerar PDF é caro), além do 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:

EtapaResultado
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

Solução de problemas

SintomaCausa provável e solução
Texto com fonte diferente da telaO 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 demaisO 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 apareceO 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áginaNão há paginação automática. Divida em mais páginas.
Estilos da sua aplicação "vazam" para a páginaO contêiner herda do <body>. Escope o CSS da página e defina fonte, cor e line-height na raiz.
Dompdf is not installedRode composer install ou carregue o vendor/autoload.php antes.
Options::$tempDir must be an existing writable directoryCrie 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

PacoteVersãoLicençaPara que serve
dompdf/dompdf^3.1 (testado 3.1.6)LGPL-2.1Só 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-lib1.0.2LGPL-2.1-or-laterLê os TTF e gera o subconjunto embutido no PDF.
dompdf/php-svg-lib1.0.2LGPL-3.0-or-laterDependência do Dompdf (não usada diretamente).
masterminds/html52.11.0MITDependência do Dompdf (não usada diretamente).
sabberworm/php-css-parser9.5.0MITDependência do Dompdf (não usada diretamente).
Extensão gd—PHPDecodificação, redução, filtros e sombras de imagens.
Extensão mbstring—PHPTexto UTF-8.
Fontes em fonts/—SIL OFL 1.1Embutidas nos PDFs. Avisos em fonts/OFL.txt.

No navegador: nenhuma dependência (js/jh2pdf.js é JavaScript puro).

Só em desenvolvimento

FerramentaLicençaPara que serve
ComposerMITInstalar o Dompdf e rodar composer test.
Python 3 + fonttools + brotliMITtools/generate-fonts.py: gerar TTF estáticos a partir de fontes variáveis.
LGPL na prática: o Dompdf é usado como biblioteca, sem alterações, instalado pelo Composer. Isso é compatível com a licença MIT do Jh2Pdf e com uso em software fechado. A LGPL só exige que, se você modificar o próprio Dompdf e distribuir, publique essas modificações, e que o usuário possa trocar a versão do Dompdf (o que o Composer já permite).
Nomes reservados de fontes (RFN): Lora, Merriweather, Playfair Display e Source Serif 4 (nome reservado "Source") têm "Reserved Font Name". As nossas instâncias estáticas são versões modificadas (geradas das variáveis), e a OFL proíbe que versões modificadas usem o nome reservado. Por isso elas vêm renomeadas como Jh Serif, Jh Serif Text, Jh Display e Jh Serif Book, com os avisos de copyright originais mantidos em 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