HotPDF convierte HTML en páginas PDF mediante THPDFHTMLImporter.RenderHTML5, un perfil de medios paginados versionado con un DOM real, una cascada CSS real, tablas, saltos de página y generación opcional de AcroForm a partir de controles de formulario HTML. Se ejecuta enteramente dentro del proceso: sin motor de navegador, sin Chromium, sin conversor externo, y por defecto sin ningún acceso a archivos ni a la red
La razón de que exista un punto de entrada aparte en lugar de una mejora de RenderHTML es la compatibilidad. El método antiguo lleva años renderizando facturas y cartas en aplicaciones de producción, y un DOM permisivo con una cascada completa cambia la salida de documentos que se ajustaron al comportamiento anterior. Así que el método antiguo conserva su semántica exacta y todo lo nuevo vive detrás de RenderHTML5, cuya versión de perfil se reporta mediante HTML5ProfileVersion y cuyos hitos soportados se listan mediante HTML5ProfileMilestones
¿Qué añade realmente el perfil HTML5?
En la práctica importan cuatro capacidades. Un DOM estructural donde html y body son contenedores y no nodos de texto. Una cascada CSS con la precedencia correcta. Maquetación de tablas. Y control explícito de saltos de página, que es lo que significa «medios paginados»: HTML se diseñó para un viewport con desplazamiento, y convertirlo en páginas exige decidir dónde se permite que el contenido se corte
El asunto de los contenedores suena a detalle de implementación, pero en realidad es la diferencia entre que algo funcione o no. Si los elementos raíz se tratan como contenedores de texto, todo el DOM se colapsa en un único bloque de texto durante el análisis, y a partir de ahí cada regla de hoja de estilos, cada salto de página, cada tabla y cada subárbol de formulario se aplica sobre nada. Cualquier renderizador de HTML que produzca un único muro de texto indiferenciado a partir de un documento estructurado ha cometido exactamente este error
El orden de la cascada, explicado paso a paso
La precedencia de CSS no es una única ordenación. HotPDF aplica las declaraciones normales de las reglas coincidentes por orden de especificidad y luego por orden de origen, aplica encima las declaraciones normales en línea, después las declaraciones !important de las reglas coincidentes, y por último las !important en línea. Cuatro pasadas, en ese orden
Las implementaciones que reducen esto a «gana el último» aciertan en los casos comunes y fallan en los interesantes, lo cual es peor que fallar de forma uniforme, porque el fallo solo aparece en las plantillas que usan !important para sobrescribir una hoja de estilos base — precisamente las plantillas escritas por personas que esperan que la cascada funcione:
uses
HPDFDoc, HPDFHTMLImport;
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Html, Css: WideString;
begin
Pdf := THotPDF.Create(nil);
Importer := THPDFHTMLImporter.Create(Pdf);
try
Pdf.FileName := 'statement.pdf';
Pdf.BeginDoc;
Importer.Margin := 56; // puntos
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Importer.CreateAcroForms := False; // renderiza los controles, no genera campos
Css := '@page { margin: 56pt } ' +
'table { border-collapse: collapse } ' +
'tr.total { page-break-inside: avoid; font-weight: bold }';
Html := LoadTemplate('statement.html');
if not Importer.RenderHTML5(Html, Css) then
raise Exception.Create('nothing was emitted - check the markup');
Pdf.EndDoc;
Writeln(Format('profile=%d nodes=%d rules=%d layout ops=%d',
[Importer.Statistics.ProfileVersion,
Importer.Statistics.ParsedNodeCount,
Importer.Statistics.CSSRuleCount,
Importer.Statistics.LayoutOperationCount]));
finally
Importer.Free;
Pdf.Free;
end;
end;
El segundo argumento es una hoja de estilos de autor que se aplica junto con lo que el documento ya lleva, lo cual es la forma limpia de imponer normas propias, como márgenes de página y control de viudas, sin editar cada plantilla
¿Por qué el importador nunca hace peticiones de ningún tipo?
Un conversor de HTML a PDF que resuelve referencias externas es, en el fondo, un motor de server-side request forgery disfrazado de generador de documentos. Apúntalo a marcado suministrado por el usuario y solicitará sin dudarlo http://169.254.169.254/ o una URL de archivo de tu servidor de aplicaciones, e incrustará el resultado en un PDF que alguien descargará
Por eso HotPDF no lee archivos ni la red por defecto. Las imágenes y demás recursos llegan exactamente por dos vías: como URI data: dentro del marcado, o mediante el callback OnResource, donde tu código decide qué significa una URI y suministra los bytes:
procedure TReportForm.SupplyResource(Sender: TObject;
const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
Handled := False;
// Sirve únicamente desde un directorio local de assets en lista blanca.
// Nunca pases la URI a un cliente HTTP, y nunca la combines con una
// ruta sin validarla primero
if StartsText('asset:', URI) then
begin
Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
ContentType := 'image/png';
Handled := Length(Data) > 0;
end;
end;
Cada recurso que sí llega se sigue contabilizando contra tres límites independientes: cuántos recursos, cuántos bytes tiene cada uno, y cuántos bytes en total. Un callback que devuelve fielmente lo que se le pide no puede ser inducido a agotar la memoria por un documento que referencia la misma imagen de 50 MB doscientas veces
Cuatro presupuestos, un único modo de fallo
Los nodos del analizador, las reglas CSS, las operaciones de maquetación y los recursos se miden de forma independiente, porque fallan de forma independiente. Un documento puede ser pequeño y patológicamente costoso de maquetar; otro puede ser superficial pero con una hoja de estilos diseñada para hacer explotar la coincidencia de selectores. Agotar cualquiera de ellos lanza EHPDFHTMLImportBudget, una clase de excepción dedicada para que el agotamiento del presupuesto pueda capturarse por separado de una entrada malformada
Las estadísticas se reinician al comienzo de cada llamada a RenderHTML5, de modo que los números describen una importación y no la vida útil del objeto importador. En un servicio que renderiza plantillas no confiables, registra ParsedNodeCount, LayoutOperationCount y ResourceRejectCount por documento: un recuento de rechazos que aumenta suele significar que el autor de la plantilla está referenciando assets que tu callback no reconoce, y un recuento de operaciones de maquetación cercano al límite significa que el siguiente documento, apenas un poco más grande, fallará
Elegir entre la importación de HTML y la API de maquetación
La importación de HTML gana cuando el contenido lo redactan como HTML personas que no son desarrolladores de Delphi, cuando las plantillas cambian con más frecuencia que el código, o cuando el mismo contenido también debe enviarse como correo electrónico. El árbol de maquetación declarativa descrito en maquetación PDF declarativa con salida etiquetada gana cuando el documento lo genera tu propio código a partir de datos, cuando necesitas que la estructura PDF/UA se emita automáticamente, o cuando el control preciso sobre la paginación importa más que la comodidad de redacción
No son excluyentes. Una combinación habitual es usar importación de HTML para el cuerpo de un informe, con los bloques de encabezado, pie y firma dibujados directamente, ya que esos tienen una geometría fija que ninguna plantilla debería controlar. Las tablas son el único terreno donde vale la pena medir ambas opciones: las tablas financieras complejas a veces paginan mejor mediante la ruta dedicada de tablas descrita en generar tablas en PDF
Define CreateAcroForms de forma deliberada. Desactivado, los controles de formulario HTML se renderizan como arte estático, que es lo que quiere un extracto impreso. Activado, se convierten en campos AcroForm interactivos reales, que es lo que quiere un formulario rellenable, y el comportamiento de los campos que se deriva de ello se trata en añadir campos de formulario a un PDF cargado
La importación de HTML, la maquetación declarativa y la API de dibujo directo actúan todas sobre el mismo objeto documento en Delphi y C++Builder; la lista completa de funciones está en la página del componente HotPDF Delphi PDF