Artículo técnico

Grillas de rowspan y encabezados repetidos en HotPDF

HotPDF renderiza tablas HTML a través de su perfil de paged-media HTML5 usando una grilla de ocupación real para rowspan y colspan, alturas de fila medidas en lugar de estimaciones por conteo de caracteres, y filas de encabezado repetidas en cada página de continuación. Hay dos situaciones en las que se niega a repetir un encabezado, y conocerlas por adelantado cuesta menos que depurar una celda duplicada después

La clase de documento que fuerza esto es la que todo equipo de reportes termina distribuyendo: una factura o un reporte de cumplimiento donde la fuente de verdad es HTML, la tabla cruza cuatro páginas, y el encabezado tiene que ser legible en cada una. Cualquier cosa menos un layout de tabla real produce las dos fallas que los lectores notan de inmediato: un encabezado que aparece una sola vez en la página uno y filas cuyas alturas fueron adivinadas por conteo de caracteres

¿Por qué la capacidad de tablas se movió al renderer de HTML?

Porque la alternativa pierde rich text, y el rich text es la razón por la que el contenido es HTML para empezar. El plan obvio suena a reutilización: HotPDF ya tiene un objeto de tabla del layout DOM con una grilla propiamente dicha, así que hay que tender un puente desde el parser de HTML hacia él y heredar el spanning gratis. El problema es con qué dibuja ese objeto de tabla. Sus celdas cargan texto y un estilo, y su ruta de dibujo emite salida de texto plano, así que cualquier cosa que el HTML realmente contuviera más allá de una fuente y un color, links, superíndices, cambios de tamaño en línea, color por run, se pierde para cuando llega a la página

La dirección que sobrevive al contacto con documentos reales es la inversa. Mover las capacidades del motor de tablas, la grilla de ocupación, la medición real, la repetición de encabezados y la ponderación de columnas, hacia el renderer de HTML, y dejar el renderizado de rich text donde ya funciona. Es un cambio más grande que el puente, y es el cambio que mantiene a un hyperlink dentro de una celda de tabla como un hyperlink

Rowspan sin union-find

Las celdas con spanning crean grupos de filas atómicos, pero el cierre sobre esos grupos no necesita una estructura de disjoint sets general, porque la ocupación siempre es un intervalo contiguo. Una celda con rowspan="3" que empieza en la fila K ocupa las filas K a K+2 y nada más, así que la información de grupo se reduce a un marcador de fin por fila

El algoritmo son dos líneas de intención. Cuando coloca una celda con spanning que empieza en K y termina en E, registre GroupEnd[K] := Max(GroupEnd[K], E). Luego recorra las filas una vez en reversa y aplique G[R] := G[G[R]], que propaga cada fin de fila hacia atrás a través de los spans superpuestos y produce el cierre transitivo en una sola pasada. Lo que obtiene es, para cada fila, la última fila que debe quedarse en la misma página con ella, que es exactamente lo que el paso de paginación necesita para decidir dónde puede caer un salto

Distribuir la altura es la otra mitad. Cuando una celda con spanning necesita más espacio vertical que el que las filas que cubre proveen actualmente, el sobrante va a la última fila del span, no se reparte parejo entre ellas. Procese las celdas con spanning después de que las alturas de fila ordinarias queden asentadas, y luego complete la fila final de cada span. Repartir el sobrante parejo parece más justo y produce salida visiblemente equivocada: filas que solo contienen celdas cortas de una línea se inflan porque alguna celda sin relación tres filas arriba resultó alta

Una grilla de tabla HTML de HotPDF donde una celda con rowspan 3 que empieza en la fila 2 ocupa las filas 2 a 4 como un solo rectángulo atómico, junto a los valores de fin de grupo por fila G de R producidos por un recorrido en reversa que muestra las filas 2, 3 y 4 atadas a la misma página
La ocupación con spanning siempre es un intervalo contiguo, así que marcadores de fin por fila y un recorrido en reversa reemplazan al union-find y le dicen a la paginación exactamente dónde puede caer un salto
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Stats: THPDFHTMLImportStatistics;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'audit-report.pdf';
    Pdf.BeginDoc;
    Importer := THPDFHTMLImporter.Create(Pdf);
    try
      Importer.Margin := 48;
      Importer.BaseFontName := 'Arial';
      Importer.BaseFontSize := 10;
      Importer.MaxDOMNodes := 200000;
      Importer.MaxLayoutOperations := 2000000;
      if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
      begin
        Stats := Importer.Statistics;
        Writeln('tables ', Stats.TableCount,
                '  page breaks ', Stats.PageBreakCount);
      end;
    finally
      Importer.Free;
    end;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

RenderHTML5 toma una hoja de estilos de autor opcional como segundo argumento, que es donde pertenecen las reglas de impresión. Mantenga fuera la hoja de estilos de pantalla. El perfil está versionado, y HTML5ProfileMilestones reporta qué grupos de capacidades implementa la compilación actual, himParserCascade, himPagedLayout, himTablesForms y himBoundedResources, de modo que una aplicación puede degradar deliberadamente en lugar de descubrir un hueco en producción

La medición tiene que coincidir con el dibujo, exactamente

La altura de fila solo es correcta cuando el código que mide las líneas envueltas las envuelve con la misma regla que el código que las dibuja. Suena obvio y es la fuente más común de tablas cuyos bordes no se alinean con su contenido. HotPDF mide con un contador de líneas greedy, y ese contador tiene que coincidir con la semántica de envoltura de la ruta de salida de rich text en tres respectos específicos: corta solo en espacios, nunca parte una palabra, y una palabra más ancha que la columna obtiene una línea propia

El segundo requisito es la fuente. La medición debe correr con la fuente de la propia celda, fijada vía SetFont con el nombre real, el set de estilo y el tamaño antes de llamar a la función de ancho, no con la fuente que casualmente estuviera activa. El texto en bold suele ser más del diez por ciento más ancho que el regular al mismo tamaño, lo que basta para convertir una celda de tres líneas en una de cuatro. Una tabla donde las celdas de encabezado van en bold y las del cuerpo no, medida con una sola fuente, estará mal justo en las filas que los lectores miran primero

Hacer esto bien cambia lo que puede afirmar en un test. El efecto observable de una medición precisa es el interlineado, no los conteos de glifos: una fila de una sola línea mide unos 20 puntos, mientras que una estimación por conteo de caracteres del mismo contenido predice dos líneas y unos 35. Afirme sobre la distancia vertical entre filas. Y recuerde que el user space de PDF tiene la Y creciendo hacia arriba, así que un encabezado parado sobre una fila del cuerpo significa que el valor Y del encabezado es el mayor, lo contrario de lo que el instinto de coordenadas de pantalla escribe

¿Cuándo se niega HotPDF a repetir un encabezado?

En dos casos, ambos de los cuales producirían salida visiblemente equivocada si siguiera adelante. El primero es un bloque de encabezado que contiene una celda con spanning que se extiende más allá del encabezado hacia filas del cuerpo. Repetir el encabezado dibujaría ese contenido de celda una segunda vez en una posición donde ya no pertenece, así que el encabezado se dibuja una vez y la tabla continúa sin él. El segundo es un encabezado más alto que el 90 por ciento de la altura útil de la página, donde la repetición dejaría casi sin lugar para datos y la tabla no avanzaría

El flujo de decisión de HotPDF para repetir encabezados de tabla HTML entre saltos de página: un encabezado cuyo rowspan cruza hacia filas del cuerpo se dibuja una vez, un encabezado más alto que el 90 por ciento de la altura útil de la página se dibuja una vez, y todo otro encabezado se repite en cada página de continuación
Los dos rechazos son deliberados: repetir un encabezado que posee una celda con spanning en el cuerpo o que llena casi toda la página dibujaría contenido donde ya no pertenece o dejaría sin lugar para los datos

Ambos rechazos son deliberados y silenciosos a propósito, porque la alternativa es peor. Si su encabezado no se repite y usted esperaba que lo hiciera, revise el markup buscando un rowspan que cruce el límite del thead antes de sospechar del motor. Ese único patrón de markup explica la mayoría de las sorpresas

// Los pesos de columna vienen del markup, así que la hoja de estilos de
// impresión es el lugar para controlarlos. Los anchos son pesos, no píxeles
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Una fila de encabezado con un rowspan que cruza hacia el cuerpo suprime
// la repetición del encabezado. Mantenga los spans dentro de una sección:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  cruza al tbody, sin repetición

Los anchos de columna se comportan como pesos en lugar de mediciones absolutas, que es el comportamiento que mantiene usable una tabla cuando el contenido no coincide con la estimación del autor. Una columna declarada al 30 por ciento recibe aproximadamente el 30 por ciento del ancho disponible, pero la distribución respeta el ancho mínimo que cada columna realmente necesita, así que una columna angosta que sostiene un token largo e inquebrantable no desborda silenciosamente la caja de la tabla

Dónde encaja esto en un pipeline de documentos

El trabajo de tablas vive dentro del perfil de paged-media más amplio, y las reglas de paginación, los presupuestos de recursos y el manejo de CSS descritos en la ruta de importación paged-media de HTML5 aplican sin cambios a documentos que contienen tablas. Si sus datos no empiezan como HTML, la ruta de construcción directa en construir tablas directamente en un PDF evita la capa de parseo por completo y le da el mismo comportamiento de grilla vía una API. Y como la altura de fila al final depende de dónde cortan las líneas, la discusión de medición en justificación de texto y corte de líneas es la pieza complementaria para quien afine salida tabular densa

La lección reutilizable aquí no va de tablas. Cuando un subsistema nuevo necesita una capacidad que un subsistema viejo ya tiene, pregúntese cuál de los dos posee lo más difícil de reimplementar. La aritmética de la grilla son unas pocas docenas de líneas y se muda con facilidad. El renderizado de rich text con links en línea, superíndices y estilos por run no lo es, así que la grilla se mudó y el texto se quedó. HotPDF distribuye ambas rutas como parte del HotPDF Delphi PDF component, así que la elección entre entrada HTML y construcción directa es una decisión de proyecto, no de biblioteca