Artículo técnico

Rejillas rowspan y cabeceras de tabla repetidas en HotPDF

HotPDF renderiza tablas HTML a través de su perfil HTML5 de medios paginados usando una rejilla de ocupación real para rowspan y colspan, alturas de fila medidas en lugar de estimaciones por recuento de caracteres, y filas de cabecera repetidas en cada página de continuación. Dos situaciones hacen que se niegue a repetir una cabecera, y conocerlas de antemano sale más barato que depurar después una celda duplicada

La clase de documento que fuerza esto es la que tarde o temprano publica cualquier equipo de informes: una factura o un informe de cumplimiento donde la fuente de verdad es HTML, la tabla cruza cuatro páginas, y la cabecera tiene que ser legible en todas. Cualquier cosa menos una maquetación de tabla real produce los dos fallos que los lectores notan de inmediato: una cabecera que aparece una sola vez en la página uno y filas cuyas alturas se adivinaron por recuento de caracteres

¿Por qué la capacidad de tablas se mudó al renderizador HTML?

Porque la alternativa pierde texto enriquecido, y el texto enriquecido es la razón por la que el contenido es HTML en primer lugar. El plan obvio parece reutilización: HotPDF ya tiene un objeto de tabla del DOM de maquetación con una rejilla en regla, así que basta tender un puente desde el parser HTML hacia él y conseguir el spanning gratis. El problema es con qué dibuja ese objeto de tabla. Sus celdas llevan 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, enlaces, superíndices, cambios de tamaño inline, color por tramo, ya no está cuando llega a la página

La dirección que sobrevive al contacto con documentos reales es la contraria. Mover las capacidades del motor de tablas, la rejilla de ocupación, la medida real, la repetición de cabeceras y la ponderación de columnas, al renderizador HTML, y dejar el renderizado de texto enriquecido donde ya funciona. Es un cambio más grande que el puente, y es el cambio que mantiene un hiperenlace dentro de una celda de tabla como hiperenlace

Rowspan sin union-find

Las celdas con spanning crean grupos de fila atómicos, pero el cierre sobre esos grupos no necesita una estructura general de conjuntos disjuntos, porque la ocupación es siempre 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 acaba en E, registre GroupEnd[K] := Max(GroupEnd[K], E). Luego recorra las filas una vez en sentido inverso y aplique G[R] := G[G[R]], lo que propaga cada fin de fila hacia atrás a través de los spans solapados 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

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

Una rejilla 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 único rectángulo atómico, junto a los valores de fin de grupo por fila G de R producidos por un único recorrido inverso que muestra las filas 2, 3 y 4 ligadas a la misma página
La ocupación con spanning es siempre un intervalo contiguo, así que los marcadores de fin por fila y un recorrido inverso sustituyen a union-find y 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. Deje fuera la hoja de estilos de pantalla. El perfil está versionado, y HTML5ProfileMilestones informa de qué grupos de capacidad 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 medida 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 cuadran con su contenido. HotPDF mide con un contador de líneas avaro, y ese contador tiene que coincidir con la semántica de ajuste de línea de la ruta de salida de texto enriquecido en tres respectos concretos: rompe solo en espacios, nunca parte una palabra, y una palabra más ancha que la columna recibe una línea para ella sola

El segundo requisito es la fuente. La medida tiene que correr con la fuente de la propia celda, fijada mediante SetFont con el nombre real, el estilo y el tamaño antes de llamar a la función de ancho, no con la fuente que por casualidad estuviera activa. El texto en negrita suele ser más de un 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 cabecera van en negrita y las del cuerpo no, medida con una sola fuente, estará mal justo en las filas que los lectores miran primero

Acertar esto cambia lo que puede afirmar en un test. El efecto observable de una medida exacta es el interlineado, no los recuentos de glifos: una fila de una línea mide unos 20 puntos mientras que una estimación por recuento de caracteres del mismo contenido predice dos líneas y unos 35. Afirme sobre la distancia vertical entre filas. Y recuerde que el espacio de usuario del PDF tiene la Y creciendo hacia arriba, así que una cabecera situada encima de una fila del cuerpo significa que su valor Y es el mayor, que es lo contrario de lo que el instinto de coordenadas de pantalla escribe

¿Cuándo se niega HotPDF a repetir una cabecera?

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

El flujo de decisión de HotPDF para repetir cabeceras de tabla HTML entre saltos de página: una cabecera cuyo rowspan cruza hacia filas del cuerpo se dibuja una vez, una cabecera más alta que el 90 por ciento de la altura útil de página se dibuja una vez, y cualquier otra cabecera se repite en cada página de continuación
Los dos rechazos son deliberados: repetir una cabecera que posee una celda con spanning del cuerpo o que llena la mayor parte de la página dibujaría contenido donde ya no pertenece o no dejaría sitio para datos

Ambos rechazos son deliberados y silenciosos por diseño, porque la alternativa es peor. Si su cabecera no se repite y usted esperaba que lo hiciera, compruebe en el marcado si hay un rowspan que cruce el límite del thead antes de sospechar del motor. Ese único patrón de marcado explica la mayoría de las sorpresas

// Los pesos de columna vienen del marcado: la hoja de estilos de impresión
// es el sitio donde 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 cabecera con rowspan que cruza al cuerpo suprime la repetición.
// Mantenga los spans dentro de una sección:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  abarca el tbody, sin repetición

Los anchos de columna se comportan como pesos y no como medidas absolutas, que es el comportamiento que mantiene una tabla usable 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 el reparto respeta el ancho mínimo que cada columna realmente necesita, así que una columna estrecha que contenga un token largo indivisible no desborda silenciosamente la caja de la tabla

Dónde encaja esto en un pipeline de documentos

El trabajo de tablas forma parte del perfil más amplio de medios paginados, y las reglas de paginación, los presupuestos de recursos y el manejo de CSS descritos en la ruta de importación HTML5 de medios paginados se aplican sin cambios a los documentos que contienen tablas. Si sus datos no empiezan siendo HTML, la ruta de construcción directa de construir tablas directamente en un PDF evita por completo la capa de análisis y le da el mismo comportamiento de rejilla mediante una API. Y como la altura de fila depende en última instancia de dónde rompen las líneas, la discusión de medida en la justificación de texto y el corte de líneas es la pieza complementaria para quien ajuste salida tabular densa

La lección reutilizable aquí no va de tablas en absoluto. 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 rejilla son unas pocas docenas de líneas y se traslada con facilidad. El renderizado de texto enriquecido con enlaces inline, superíndices y estilos por tramo no, así que la rejilla se mudó y el texto se quedó. HotPDF envía ambas rutas como parte del componente PDF Delphi HotPDF, de modo que la elección entre entrada HTML y construcción directa es una decisión de proyecto y no de biblioteca