Artículo técnico

HTML a PDF en Delphi: la ruta de medios paginados HTML5

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 completamente en el mismo proceso: sin motor de navegador, sin Chromium, sin convertidor externo, y por defecto sin ningún acceso a archivos ni a la red

La razón por la que existe un punto de entrada separado en lugar de un RenderHTML mejorado es la compatibilidad. El método anterior 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 fueron ajustados contra el comportamiento anterior. Por eso el método anterior 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 admitidos se listan mediante HTML5ProfileMilestones

¿Qué agrega realmente el perfil HTML5?

En la práctica importan cuatro capacidades. Un DOM estructural donde html y body son contenedores en lugar de nodos de texto. Una cascada CSS con la precedencia correcta. Diseño de tablas. Y un control explícito de saltos de página, que es lo que significa "medios paginados": HTML fue diseñado para una ventana de desplazamiento, y convertirlo en páginas exige decidir dónde se le permite al contenido cortarse

El punto de los contenedores suena como un detalle de implementación y 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 colapsa en un solo bloque de texto durante el análisis, y cada regla de hoja de estilos, salto de página, tabla y subárbol de formulario posterior se aplica sobre nada. Cualquier renderizador HTML que produzca un 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 un solo ordenamiento. HotPDF aplica las declaraciones normales de las reglas coincidentes en orden de especificidad y luego en orden de origen, aplica encima las declaraciones normales en línea, luego las declaraciones !important de las reglas coincidentes, y finalmente las !important en línea. Cuatro pasadas, en ese orden

HotPDF resuelve cada propiedad CSS en Delphi mediante cuatro pasadas de cascada ordenadas, desde la coincidencia de reglas normales hasta las declaraciones !important en línea
Las pasadas posteriores ganan los empates de forma deliberada: las declaraciones en línea superan a las hojas de estilos, y !important supera a todo lo declarado normalmente

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 manera uniforme, porque la falla 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;  // renderizar los controles, sin crear 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 del autor que se aplica junto con lo que traiga el documento, y es la forma limpia de imponer reglas internas, como los márgenes de página y el control de líneas huérfanas, sin editar cada plantilla

¿Por qué el importador nunca hace solicitudes externas?

Un convertidor 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úntelo a marcado suministrado por el usuario y con gusto hará una solicitud a http://169.254.169.254/ o a una URL de archivo desde su servidor de aplicaciones, e incrustará el resultado en un PDF que alguien descargará

Por eso, de forma predeterminada, HotPDF no lee archivos ni la red. Las imágenes y otros recursos llegan exactamente de dos maneras: como URI data: dentro del marcado, o a través del callback OnResource, donde su código decide qué significa una URI y suministra los bytes:

El importador de HTML para Delphi de HotPDF resuelve cada URI de recurso de forma local, aceptando URI data y callbacks OnResource, mientras rechaza solicitudes de archivos y de red, todo detrás de límites de cantidad y de bytes
Solo los bytes que llegan de forma local alcanzan la página, las referencias rechazadas incrementan el contador, y ninguna solicitud sale jamás del proceso
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Servir solo desde un directorio local de recursos permitidos
  // (allow-list). Nunca pasar la URI a un cliente HTTP, y nunca
  // combinarla 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 igual se cuenta 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 límites, un solo modo de falla

Los nodos del analizador, las reglas CSS, las operaciones de diseño y los recursos se miden de forma independiente, porque fallan de forma independiente. Un documento puede ser pequeño y patológicamente costoso de diagramar; 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, de modo que el agotamiento del límite se pueda capturar por separado de un marcado mal formado

Las estadísticas se reinician al comienzo de cada llamada a RenderHTML5, de modo que los números describen una importación, no la vida completa del objeto importador. En un servicio que renderiza plantillas no confiables, registre ParsedNodeCount, LayoutOperationCount y ResourceRejectCount por documento: un conteo de rechazos en aumento suele significar que el autor de la plantilla está referenciando recursos que su callback no reconoce, y un conteo de operaciones de diseño cercano al límite significa que el siguiente documento, apenas un poco más grande, fallará

Cómo elegir entre la importación de HTML y la API de diseño

La importación de HTML gana cuando el contenido lo redactan en HTML personas que no son desarrolladores 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 diseño declarativo descrito en diseño declarativo de PDF con salida etiquetada gana cuando el documento lo genera su propio código a partir de datos, cuando necesita 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. Un patrón común es usar importación de HTML para el cuerpo de un informe, con el encabezado, el pie de página y los bloques de firma dibujados directamente, ya que esos tienen una geometría fija que ninguna plantilla debería controlar. Las tablas son la única área donde vale la pena medir ambos enfoques: las tablas financieras complejas a veces paginan mejor mediante la ruta dedicada de tablas descrita en generar tablas en PDF

Configure CreateAcroForms de forma deliberada. Desactivado, los controles de formulario HTML se renderizan como arte estático, que es lo que necesita un estado de cuenta impreso. Activado, se convierten en campos AcroForm realmente interactivos, que es lo que necesita un formulario rellenable, y el comportamiento de campo que sigue se explica en agregar campos de formulario a un PDF cargado

La importación de HTML, el diseño declarativo y la API de dibujo directo apuntan todos al mismo objeto de documento en Delphi y C++Builder; la lista completa de funciones está en la página del componente PDF para Delphi de HotPDF