HTML a PDF idéntico al navegador, sin navegador en el servidor. El navegador de quien exporta calcula el diseño; PHP solo dibuja el PDF. Funciona en hosting compartido (PHP + GD).
Ver la galería de ejemplosFormato del JSON (format.md)Licencia MITVisión general
Generar PDF a partir de HTML suele terminar en uno de estos callejones sin salida:
| Opción | Problema |
|---|---|
| Chrome / Puppeteer en el servidor | necesita un VPS o contenedor; no funciona en hosting compartido |
| wkhtmltopdf | binario en el servidor, CSS antiguo, proyecto descontinuado |
| Dompdf / mPDF / TCPDF leyendo el HTML | solo entienden CSS 2.1: sin flexbox, grid, variables, clamp(), filtros… el PDF sale distinto de la pantalla |
| API externa de PDF | costo, dependencia de terceros y el contenido sale de tu servidor |
Jh2Pdf parte de otra idea: el navegador de quien exporta ya es el mejor motor de diseño que existe, y ya está abierto. En lugar de reimplementar CSS en PHP, el navegador calcula dónde va cada palabra, caja, borde e imagen, y PHP solo dibuja esas posiciones. No hay saltos de línea ni cálculo de diseño en el servidor que puedan fallar.
calc(): todo lo que el navegador entiende, porque es él quien mide.Cómo funciona
NAVEGADOR (js/jh2pdf.js) SERVIDOR (PHP)
──────────────────────── ──────────────
tu HTML + tu CSS Options (tempDir, fontDir, imageResolver…)
│ dibuja cada página fuera de la pantalla │
│ (espera fuentes e imágenes) ▼
▼ Layout::clean() valida todo (el JSON es hostil)
extractLayout() recorre el DOM: │
getComputedStyle + Range.getClientRects por palabra ▼
│ Renderer::render() → Dompdf\Cpdf
▼ R rectángulo · B bordes · T texto · I imagen
JSON de operaciones ─────── POST ───────────────────► M marca a mano · L línea · C/E recorte
{ w, h, pages: [{ bg, ops: [...] }] } │
▼
bytes del PDF
- Medición (navegador). Cada página se inserta en un contenedor fuera de la pantalla, dentro de tu propio documento, así que tu CSS se aplica.
El módulo espera las fuentes (
document.fonts.ready) y las imágenes (hasta 4 s cada una). - Extracción. Para cada elemento visible lee el estilo calculado y las cajas; para cada palabra, la posición y el ancho exactos. El resultado es una lista ordenada de operaciones de dibujo (el orden es el orden de pintado).
- Dibujo (servidor). PHP valida el JSON y dibuja las operaciones con el
Cpdfde Dompdf, usado solo como "lienzo" de PDF. Cada palabra se coloca en su línea base y su espaciado entre letras se ajusta para tener exactamente el ancho medido.
Requisitos
| Dónde | Requisito | Observación |
|---|---|---|
| Navegador | cualquier navegador moderno (Chromium, Firefox, Safari) | usa módulos ES, Range.getClientRects, document.fonts |
| Servidor | PHP ≥ 8.1 | probado en 8.2 |
| Servidor | extensiones gd y mbstring | GD: imágenes, filtros y sombras · mbstring: texto UTF-8 |
| Servidor | dompdf/dompdf ^3.1 | instalado con Composer (ver Dependencias) |
| Servidor | una carpeta con permiso de escritura | caché de fuentes e imágenes temporales (Options::$tempDir) |
Instalación
Con Composer
composer require geisonq/jh2pdf
O, desde este repositorio: composer install en la raíz (instala Dompdf en vendor/).
Sin Composer
Copia la carpeta y usa el autoloader incluido. Dompdf sigue siendo necesario y tienes que cargarlo tú:
require '/ruta/jh2pdf/autoload.php'; // clases Jh2Pdf\*
require '/ruta/vendor/autoload.php'; // Dompdf
En el navegador
Copia js/jh2pdf.js a tu sitio. Es un módulo ES sin dependencias (~30 KB, sin minificar).
Inicio rápido
1. Navegador
import { extractLayout } from './jh2pdf.js';
const layout = await extractLayout(
['<h1>Hola</h1><p>Primera página</p>', '<p>Segunda página</p>'], // un fragmento de HTML por página
{ width: 794, height: 1123 } // A4 vertical en 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); // limita el cuerpo
$layout = json_decode((string)$raw, true);
if (!is_array($layout)) { http_response_code(400); exit; }
$options = new Jh2Pdf\Options('/ruta/con/escritura');
try {
$pdf = (new Jh2Pdf\Renderer($options))->render($layout, 'Título del documento');
} catch (Jh2Pdf\Exception $e) {
error_log((string)$e->getPrevious()); // el detalle está en la excepción anterior
http_response_code(500); exit;
}
header('Content-Type: application/pdf');
echo $pdf;
Ejemplos ejecutables: examples/minimal/ (lo mínimo) y demo/ (galería con tablas, efectos e imágenes).
API JavaScript
extractLayout(pages, options) → Promise<Layout>
pages es un array con una entrada por página: un string de HTML, un objeto { html } o cualquier valor, si pasas wrap.
| Opción | Tipo | Por defecto | Descripción |
|---|---|---|---|
width, height | número (px) | — | Obligatorios. Tamaño de la página en px CSS. En el PDF se convierte en pt × 0,75 (794×1123 px = A4). |
wrap(page, i) | función → string | <div>…</div> | HTML completo de la página, con un único elemento raíz. El background-color de la raíz se convierte en el fondo de la página. Útil para pasar elementos existentes: wrap: (el) => el.outerHTML. |
hostCss | string | — | CSS extra dentro del contenedor de medición (por ejemplo, un reset). Limítalo con [data-jh2pdf-host]. |
marks | boolean | false | Dibuja las marcas a mano (.mk.mk-circle etc.). |
skip(el) | función → boolean | — | true para no dibujar un elemento ni su contenido. |
acceptImage(src) | función → boolean | — | Filtra los src de imagen que se envían. Quien decide de verdad es el servidor. |
sanitize(html) | función → string | — | Sanitizador del HTML de cada página (por ejemplo, DOMPurify). El filtro interno se ejecuta encima. Ver Seguridad. |
Devuelve { w, h, pages: [{ bg, ops }] }, listo para JSON.stringify. Lanza Error si falta width o height.
.page) y no dependas del contenedor que la rodea.
El contenedor está en position: fixed, fuera de la pantalla, y hereda del <body>. Declara fuente, color y line-height en la raíz de la página.API PHP
Namespace Jh2Pdf, PHP 8.1 con strict_types.
| Clase | Función |
|---|---|
Renderer | __construct(Options $o) · render(array $layout, string $title = ''): string. Devuelve los bytes del PDF y lanza Jh2Pdf\Exception si falla · missingFonts() lista las fuentes del último PDF que el servidor no tenía. |
Options | Configuración. Solo tempDir es obligatorio (ver Options). |
ImageResolver | Interfaz load(string $src): ?string. Tú decides de dónde vienen las imágenes; devuelve null para rechazar. |
DataUriImageResolver | Resolvedor por defecto. Solo acepta data:image/(png|jpeg|gif|webp);base64,…, hasta 12 MB (__construct(int $maxBytes)). |
Exception | Extiende RuntimeException. Mensaje genérico, sin rutas del servidor; el detalle está en getPrevious(). |
Layout | clean(array, ?Options) valida el JSON · num($v, $min, $max) y color($v) son conversores seguros · OPS = 'RBTILMCE'. |
ImageFilter | apply(string $dataUri, string $filter, …): ?string aplica filtros CSS con GD. Se puede usar por separado. |
Images | Interna: resuelve, filtra, reduce (shrink) y genera sombras (shadow). dimensionsOk() valida la cabecera antes de decodificar. |
FontFaces | css(Options $o, string|callable $url = '', string $display = 'block'): string genera el CSS @font-face del navegador a partir de las mismas fuentes que usa el PDF. |
FontIndex | Interna: la lista de fuentes que tiene el servidor (addFont + carpetas), la fuente única para el PDF y para FontFaces. |
Fonts | Interna: elige el archivo por familia, estilo y peso (reglas de CSS) y lo registra en Dompdf; cursiva simulada si la familia no la tiene. |
Marks, Svg | Internas: leen los dibujos de las marcas (SVG dentro del CSS) y convierten los trazados SVG en polígonos. |
Options
| Propiedad | Tipo | Por defecto | Descripción |
|---|---|---|---|
tempDir | string | — | Obligatorio (1.er argumento del constructor). Carpeta con escritura: caché de fuentes de Dompdf e imágenes temporales. |
fontDir | string | fonts/ de la biblioteca | Carpeta de TTF estáticos con nombre <familia>-<peso>[-italic].ttf (2.º argumento del constructor). |
addFont($family, $weight, $file, $italic) | method | — | Registra un TTF estático para una familia, un peso (100–900) y un estilo, con cualquier nombre de archivo. Tiene prioridad sobre las carpetas. Devuelve $this. |
addFontDir($dir) | method | — | Otra carpeta de TTF con nombre <familia>-<peso>[-italic].ttf. Devuelve $this. |
imageResolver | ?ImageResolver | DataUriImageResolver | De dónde vienen las imágenes. |
marksCss | ?string | resources/marks.css | Archivo CSS con los dibujos de las marcas a mano. |
creator | string | 'Jh2Pdf' | Metadato "Creator" del PDF. |
fallbackFont | string | 'DejaVu Sans' | Fuente usada cuando ninguna familia de la lista font-family existe en el servidor. Ver strictFonts. |
strictFonts | bool | false | Lanza Jh2Pdf\Exception (Missing fonts: Roboto 400.) antes de dibujar, en lugar de usar fallbackFont en silencio. Recomendado en desarrollo. |
onMissingFont | ?callable | null | Se llama una vez por cada fuente que falta, con ($family, $weight, $italic), antes de dibujar. Úsalo para registrar en el log. |
maxPages | int | 1000 | Las páginas que excedan este número se descartan. |
maxOpsPerPage | int | 60 000 | Operaciones por página. |
maxOpsTotal | int | 400 000 | Operaciones en todo el documento. |
maxImages | int | 200 | Imágenes procesadas por PDF (cada una cuesta CPU y memoria). |
memoryLimit | ?string | '512M' | Sube el memory_limit durante la generación (nunca lo baja). null = no tocarlo. |
pxToPt | float | 0.75 | Conversión de px CSS a pt (96 dpi). |
Formato del JSON
Es el contrato entre el JS y el PHP. Medidas en px CSS, origen en la esquina superior izquierda, colores #rrggbb, opacidad de 0 a 1.
La referencia completa está en docs/format.md (en portugués).
{ "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 | Nombre | Campos principales |
|---|---|---|
R | rectángulo relleno | x y w h c a r[4] |
B | bordes | x y w h s[4]=[ancho,color,a] (arriba, derecha, abajo, izquierda) r[4] |
T | texto (una palabra) | x y w h s f fb g i z c a l d (fb: familias de reserva de la lista font-family; g: peso 100–900; d: 1 subrayado, 2 tachado) |
I | imagen | x y w h u k[4] f a r[4] (u = src, k = recorte, f = filtro CSS) |
M | marca a mano | m v x y w h c z |
L | línea | x y x2 y2 c w a |
C / E | inicio / fin de recorte redondeado | x y w h r[4] (anidables hasta 32 niveles) |
CSS soportado
El diseño en sí (flexbox, grid, posicionamiento, calc(), variables, unidades) siempre está soportado, porque quien lo calcula es el navegador.
La tabla de abajo trata de lo que se dibuja.
| Recurso | Estado | Detalles |
|---|---|---|
| Texto: familia (con reservas), peso 100–900, cursiva, tamaño, color, opacidad | Sí | Usa el peso más cercano que tengas, como CSS. Las familias sin cursiva se inclinan 12°, como hace el navegador. Necesita un TTF estático en el servidor: ver Fuentes. |
text-transform, letter-spacing | Sí | El ancho final siempre es el medido por el navegador. |
text-decoration (subrayado, tachado) | Parcial | La línea se dibuja palabra por palabra (no pasa bajo los espacios). El color y el estilo de la decoración se ignoran. |
background-color, border-radius (por esquina) | Sí | |
linear-gradient | Parcial | Vertical y horizontal, con varias paradas, dibujado como franjas finas. Los ángulos diagonales se ajustan a la dirección más cercana. Sin radial, repeating ni capas. |
| Bordes (ancho y color por lado) | Parcial | dashed, dotted y double salen sólidos. Los bordes muy gruesos y redondeados pueden mostrar uniones finas. |
box-shadow sin desenfoque (exterior e interior) | Sí | Varias sombras y spread. Interior: solo desplazamiento, sin spread. |
box-shadow con desenfoque, text-shadow | No | Se ignoran. |
opacity | Sí | Multiplicada por los ancestros. |
overflow: hidden con border-radius | Sí | Recorte redondeado de todo lo que haya dentro. |
<img> PNG, JPEG, GIF, WebP | Sí | Con object-fit (cover, contain, none, scale-down) y esquinas redondeadas. |
filter en imágenes | Sí | grayscale sepia brightness contrast invert blur saturate hue-rotate opacity drop-shadow (aplicados con GD). |
Listas: disc circle square decimal lower/upper-alpha, ::marker | Sí | Incluidos start y value. |
::before/::after vacíos (content: '') | Parcial | Con fondo, borde, esquinas y sombra sólida, cuando son position: absolute o display: block. No funciona con inline-block. |
Pseudoelementos con texto, contadores, ::first-letter | No | Usa un elemento real (<span>) para números y etiquetas. |
background-image: url(), transform, clip-path, mask | No | |
SVG en línea, <canvas>, <video>, <iframe> | No | Convierte el canvas en <img src="data:…"> antes de extraer (la demo lo hace). |
| Ligaduras tipográficas | No | Por ejemplo, => en Fira Code se convierte en dos caracteres. |
| Enlaces clicables en el PDF | No | No se crea ninguna anotación ni acción (es una decisión de seguridad). |
Páginas y tamaños
Cada elemento de pages es una página del PDF, del tamaño width × height. No hay paginación automática:
lo que exceda la altura de la página se corta. Divide el contenido en páginas antes de extraer (por ejemplo, una sección por página, o midiendo alturas en el navegador).
| Formato | px CSS (width × height) | PDF (pt) |
|---|---|---|
| A4 vertical | 794 × 1123 | 595 × 842 |
| A4 horizontal | 1123 × 794 | 842 × 595 |
| Carta (Letter) | 816 × 1056 | 612 × 792 |
| Diapositiva 16:9 | 1280 × 720 | 960 × 540 |
El tamaño aceptado va de 200 a 4000 px por lado.
Fuentes
Para que el PDF salga igual que la pantalla, el navegador y el servidor deben usar los mismos archivos de fuente. Dompdf no lee woff2 ni fuentes variables, así que el servidor usa TTF estáticos, uno por peso. Hay tres formas de dárselas:
| Forma | Cuándo usarla |
|---|---|
carpeta fonts/ (Options::$fontDir) | archivos llamados <familia>-<peso>[-italic].ttf: dm-sans-600.ttf, dm-sans-400-italic.ttf (familia en minúsculas con guiones, peso de 100 a 900) |
$options->addFontDir($dir) | otra carpeta con la misma convención de nombres; úsala para tus fuentes y conserva las incluidas |
$options->addFont($family, $weight, $file, $italic) | un archivo con cualquier nombre; tiene prioridad sobre las carpetas |
$options = new Jh2Pdf\Options('/ruta/con/escritura');
$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('/mas/fuentes');
Cómo se elige la fuente
Las mismas reglas que CSS:
- Familia: la primera familia de la lista
font-familyque el servidor tiene ('Roboto', Arial, sans-serifusa Roboto, o Arial si la tienes; los nombres genéricos comosans-serifno son fuentes). El navegador envía hasta 5 de reserva después de la primera. - Estilo: cursiva si la familia tiene archivo en cursiva; si no, el estilo normal inclinado 12°, como hace el navegador.
- Peso: de 100 a 900, el archivo más cercano que tengas: pedir 800 con solo 400 y 700 usa el 700; 300 usa el 400.
Haz que el navegador use los mismos archivos
Genera el CSS @font-face a partir de las mismas fuentes, para no tener nunca dos listas que mantener iguales:
// una vez, al publicar (leer los nombres dentro de los TTF es demasiado lento para hacerlo en cada petición)
file_put_contents('public/fonts.css', Jh2Pdf\FontFaces::css($options, '/fonts/'));
o desde la línea de comandos: php tools/fonts-css.php --dir fonts --url /fonts/ --out public/fonts.css Instalado con Composer, el script está en vendor/geisonq/jh2pdf/tools/fonts-css.php.
Nunca falles en silencio
Una familia que el servidor no tiene se dibuja con fallbackFont (DejaVu Sans), y el PDF queda distinto de la pantalla. Para detectarlo:
$options->strictFonts = true; // lanza Jh2Pdf\Exception: "Missing fonts: Roboto 400, Roboto 700 italic."
$options->onMissingFont = fn(string $family, int $weight, bool $italic) => error_log("Fuente faltante: $family $weight");
$pdf = $renderer->render($layout);
$renderer->missingFonts(); // [['family' => 'Roboto', 'weight' => 400, 'italic' => false], …]
La comprobación se hace antes de dibujar nada. Usa strictFonts en desarrollo y en las pruebas; en producción, registra con onMissingFont o missingFonts().
Agregar una fuente a partir de una fuente variable
pip install fonttools brotli
python tools/generate-fonts.py --fonts fonts.json --src carpeta-de-fuentes --out fonts
Python solo se necesita para generar los archivos (--weights 300,400,700 elige los pesos; el formato de fonts.json está en la cabecera del script). Escribe la familia, el estilo y el peso correctos dentro de cada TTF. Para corregir los nombres de TTF estáticos que ya tienes: python tools/generate-fonts.py --fix-names carpeta. El servidor usa los TTF ya listos. Comprueba que la licencia de la fuente permita incrustarla en PDF.
Incluidas (12 familias, SIL OFL 1.1): Barlow Condensed, DM Sans, Fira Code, Inter, JetBrains Mono, Montserrat, Nunito, Open Sans y cuatro serif renombradas: Jh Serif (de Lora), Jh Serif Text (de Merriweather), Jh Display (de Playfair Display) y Jh Serif Book (de Source Serif 4). Con cursiva: DM Sans y Jh Serif.
Imágenes
El src de cada imagen viene del navegador y no es confiable. Por defecto solo se acepta data: (nada de disco ni de red).
Para servir imágenes de tu sistema, implementa un resolvedor y verifica que ese usuario pueda ver ese archivo:
final class MiResolvedor implements Jh2Pdf\ImageResolver
{
public function load(string $src): ?string
{
// solo tu patrón de URL; nunca concatenes el src en una ruta
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 MiResolvedor();
En el navegador, acceptImage evita enviar lo que el servidor rechazaría. Las imágenes se reducen al tamaño en que se muestran (~150 dpi):
sin transparencia pasan a JPEG, con transparencia siguen siendo PNG.
Marcas a mano
Extensión opcional, desactivada por defecto (no es HTML/CSS estándar), para resaltar texto con un trazo "hecho a mano": underline, circle, highlight, box, strike, cross.
<span class="mk mk-circle" style="--mk-color:#e5484d">importante</span>
Con marks: true, el extractor envía la caja y el color del texto marcado; PHP dibuja el SVG de la forma, que está en resources/marks.css
(o en tu propio CSS, mediante Options::$marksCss). Sin --mk-color, se usa el color del texto. Los elementos pares usan la 2.ª variante (:nth-of-type(even)), y una marca con display: inline
que se parte en varias líneas recibe un dibujo por línea. Sin marks: true, las clases .mk se tratan como cualquier otro elemento.
El CSS de pantalla (una máscara en ::after) está en demo/pages.css.
Seguridad
Hay dos fronteras, y en cada una lo que viene del otro lado se trata como hostil.
Servidor: el JSON del navegador
Cualquiera que pueda llamar a tu endpoint puede enviar un JSON hecho a mano. Por diseño, el servidor nunca interpreta HTML, CSS ni JavaScript.
| Ataque | Defensa |
|---|---|
| Inyectar HTML, JS o CSS en el texto | El texto va al PDF como glifos (en hexadecimal) y nunca se interpreta. |
Inyectar objetos PDF (/JS, /OpenAction, /URI, /Launch) | Texto en hexadecimal, título escapado y ningún objeto de acción o anotación creado. Hay una prueba para esto. |
Nombre de fuente con ../, ) o byte nulo | Reducido a [A-Za-z0-9 -] (60 caracteres; como máximo 5 familias de reserva) y buscado solo entre las fuentes que registraste. Las rutas vienen de tu configuración, nunca del JSON. |
Leer archivos locales o hacer peticiones (file://, http://, ../) | Las imágenes solo pasan por el ImageResolver. En Dompdf están desactivados isRemoteEnabled, PHP y JS, y hay chroot. |
| Bomba de imagen (PNG pequeño que declara millones de píxeles) | La cabecera se lee antes de decodificar: lado ≤ 20 000 px y ≤ 25 megapíxeles. |
| Agotar CPU o memoria | Límites de páginas, operaciones, imágenes y recortes anidados (≤ 32), topes para sombras y desenfoques, set_time_limit(180). |
Números o tipos absurdos (1e999, NaN, arrays, url(...)) | Layout::num() limita a un intervalo y Layout::color() solo acepta #rgb/#rrggbb. |
| Filtración de rutas en mensajes de error | Solo se lanza Jh2Pdf\Exception con un mensaje genérico. |
| Archivos temporales olvidados | Nombres aleatorios, borrados al final de cada PDF y barridos después de 1 hora. |
ImageResolver cuando lee archivos. Mantén Dompdf, PHP y GD actualizados.Navegador: el HTML de entrada
El HTML de cada página entra en el DOM de quien exporta. Si viene de otra persona, es una entrada hostil (XSS). El módulo primero coloca el HTML en un
<template> inerte y elimina script, iframe, object, embed, link, meta,
base, atributos on*, srcdoc, URLs javascript: y data: que no sean imagen, y expression().
Esto es un filtro de respaldo: para contenido de terceros, usa un sanitizador de verdad:
extractLayout(pages, { width, height, sanitize: (h) => DOMPurify.sanitize(h) });
<style> y url() en el CSS siguen permitidos. No ejecutan código, pero pueden disparar peticiones (rastreo).
Rendimiento
Medido con la galería de ejemplos (7 páginas A4, ~2400 operaciones, 9 imágenes con filtro) en un PC común con XAMPP y PHP 8.2:
| Etapa | Resultado |
|---|---|
| JSON enviado | ~460 KB (casi todo es la imagen data: repetida) |
| PDF generado | ~300 KB, fuentes incrustadas como subconjunto |
| PHP, primera generación | ~6–9 s (registra las fuentes en la caché de Dompdf) |
| PHP, generaciones siguientes | ~2–3 s, dominados por los filtros de imagen en GD |
- Los filtros (
blur,drop-shadow) son la parte más costosa. Sin ellos, las páginas de texto y tablas tardan fracciones de segundo. - La misma imagen con el mismo filtro y tamaño se procesa una sola vez por PDF (hay caché).
- Para documentos grandes, envía las imágenes por URL y usa un
ImageResolver, en lugar de repetirdata:en el JSON.
Solución de problemas
| Síntoma | Causa probable y solución |
|---|---|
| Texto con una fuente distinta a la de la pantalla | El servidor no tiene TTF estático para ninguna familia de la lista font-family y usó fallbackFont. Agrega la fuente (ver Fuentes); strictFonts o missingFonts() muestran cuáles faltan. |
| Palabras apretadas o demasiado espaciadas | El navegador midió con otra fuente (la de reserva o la del sistema). Carga los mismos archivos con @font-face: genera el CSS con FontFaces::css(). |
| La imagen no aparece | El src no es data: y no hay ImageResolver, o el resolvedor devolvió null, o la imagen superó los límites. |
| Contenido cortado al final de la página | No hay paginación automática. Divide en más páginas. |
| Los estilos de tu aplicación se "filtran" a la página | El contenedor hereda del <body>. Limita el CSS de la página y define fuente, color y line-height en la raíz. |
Dompdf is not installed | Ejecuta composer install o carga vendor/autoload.php antes. |
Options::$tempDir must be an existing writable directory | Crea la carpeta y da permiso de escritura al usuario de PHP. |
| Error 500 sin detalle | Es intencional. Registra $e->getPrevious() en tu log. |
Dependencias
En producción
| Paquete | Versión | Licencia | Para qué sirve |
|---|---|---|---|
| dompdf/dompdf | ^3.1 (probado 3.1.6) | LGPL-2.1 | Solo el Cpdf (escritura del PDF), las métricas y el registro de fuentes. El parser de HTML/CSS de Dompdf no se usa. |
| dompdf/php-font-lib | 1.0.2 | LGPL-2.1-or-later | Lee los TTF y genera el subconjunto incrustado en el PDF. |
| dompdf/php-svg-lib | 1.0.2 | LGPL-3.0-or-later | Dependencia de Dompdf (no se usa directamente). |
| masterminds/html5 | 2.11.0 | MIT | Dependencia de Dompdf (no se usa directamente). |
| sabberworm/php-css-parser | 9.5.0 | MIT | Dependencia de Dompdf (no se usa directamente). |
Extensión gd | — | PHP | Decodificación, reducción, filtros y sombras de imágenes. |
Extensión mbstring | — | PHP | Texto UTF-8. |
Fuentes en fonts/ | — | SIL OFL 1.1 | Incrustadas en los PDF. Avisos en fonts/OFL.txt. |
En el navegador: ninguna dependencia (js/jh2pdf.js es JavaScript puro).
Solo en desarrollo
| Herramienta | Licencia | Para qué sirve |
|---|---|---|
| Composer | MIT | Instalar Dompdf y ejecutar composer test. |
Python 3 + fonttools + brotli | MIT | tools/generate-fonts.py: generar TTF estáticos a partir de fuentes variables. |
fonts/OFL.txt.
Si generas instancias nuevas de una fuente con RFN, renómbralas también.Licencia
El código de Jh2Pdf está bajo la licencia MIT, © 2026 Geison Quevedo. Cualquier persona puede usarlo, copiarlo, modificarlo, fusionarlo, publicarlo, distribuirlo, sublicenciarlo y venderlo, incluso en proyectos cerrados, siempre que mantenga el aviso de copyright y el texto de la licencia. Las fuentes (OFL) y Dompdf (LGPL) tienen sus propias licencias, descritas en Dependencias.
Estructura
jh2pdf/
├── LICENSE MIT
├── README.md resumen e inicio rápido
├── composer.json paquete (autoload PSR-4: Jh2Pdf\ → src/)
├── autoload.php autoloader para quien no usa Composer
├── src/ PHP: Renderer, Options, Layout, ImageResolver, DataUriImageResolver,
│ Images, ImageFilter, Fonts, Marks, Svg, Exception
├── js/jh2pdf.js extractor de diseño (módulo ES, sin dependencias)
├── fonts/ TTF estáticos (OFL) + OFL.txt
├── resources/marks.css dibujos SVG de las marcas a mano
├── docs/ esta documentación (en, pt-BR, es) + format.md (el formato del JSON)
├── demo/ galería de ejemplos (index.html, pages.css, fonts.css, pdf.php)
├── examples/minimal/ el ejemplo completo más pequeño
├── tests/run.php pruebas (php tests/run.php o composer test)
├── tools/fonts-css.php genera el CSS @font-face del navegador a partir de las mismas fuentes
└── tools/generate-fonts.py genera los TTF a partir de fuentes variables
Cómo contribuir
- Pruebas:
composer installycomposer test. No hay framework de pruebas, solotests/run.php. - Criterio de calidad: quedar igual que la pantalla. Genera el PDF desde el navegador (Imprimir → Guardar como PDF) y con esta biblioteca y compáralos
lado a lado, ampliando (por ejemplo, con PyMuPDF:
page.get_pixmap(dpi=200)). - Nueva operación (por ejemplo, degradado radial): (1) el extractor en
js/jh2pdf.jsemite la operación; (2)Layout::OPSrecibe la letra; (3)Renderer::renderrecibe elcase; (4) documéntala endocs/format.md; (5) pruébala entests/run.php. Valida cada campo nuevo: es una entrada hostil. - Estilo: PHP 8.1 con
declare(strict_types=1), sin dependencias aparte de Dompdf. JS sin dependencias, con nombres de nivel superior con el prefijopl. - Las contribuciones se aceptan bajo la misma licencia del proyecto (MIT).
