Artículo técnico

Flexbox, CSS Grid y notas al pie en PDF desde Delphi

PDF Library for Delphi renderiza HTML en una página PDF con diseño bidimensional real: display: flex y display: grid se miden y colocan en lugar de degradarse a bloques apilados, y las notas al pie se reservan en la parte inferior del cuadro que lleva su referencia, con una numeración que se mantiene continua a través de columnas y páginas. Los puntos de entrada son los ya conocidos, DrawHTMLTextBox para un solo cuadro y DrawHTMLStory para flujo multicolumna

Esto importa porque el HTML es como llega ahora la mayoría del contenido de los informes. Las plantillas las escriben personas que escriben CSS, los tableros se diseñan como tarjetas, y un renderizador que colapsa en silencio una fila flex en cuatro bloques apilados produce un documento que no se parece en absoluto al diseño. Hasta que existió esta capacidad, el único contenedor bidimensional que medía el motor era la tabla, así que cada diseño de tarjeta tenía que rehacerse a mano como una tabla

¿Qué cambió en el modelo de diseño?

El bucle principal anterior mantenía un solo cuadro de línea y avanzaba hacia abajo por la página. Ese modelo maneja perfectamente el contenido en línea y los bloques apilados, y no puede expresar un contenedor cuyos hijos se dimensionan unos en relación con otros. Las tablas eran la única excepción, con su propia medición en dos pasadas

Flex y grid agregan cada uno una pasada de medición acotada sobre los hijos de un contenedor, y la palabra importante es acotada. Un contenedor flex mide hasta 256 hijos directos en un arreglo fijo. Una cuadrícula usa una matriz de ocupación de a lo sumo 64 por 64 celdas para una colocación automática determinista. Esos topes existen para que una hoja de estilos hostil o generada no pueda provocar recursión sin límite ni memoria de colocación cuadrática, lo cual es una preocupación real cuando el HTML proviene de una plantilla que edita un cliente

Cómo obtienen sus tamaños los elementos flex

En la dirección de fila, el contenedor suma la base de cada elemento junto con sus pesos de crecimiento y encogimiento, y luego distribuye el espacio sobrante, positivo o negativo, según esos pesos. Con flex-wrap, cada línea se resuelve de forma independiente, así que una fila que se divide en dos líneas asigna el espacio libre por línea y no a través de todo el contenedor. En la dirección de columna, la misma distribución del eje principal se ejecuta contra una altura explícita o la altura del contenido

justify-content, align-items, gap y las direcciones inversas operan sobre geometría que ya se midió. Mueven cuadros; nunca disparan una nueva medición del contenido del elemento. Esa separación es lo que evita que un tablero complejo mida sus hijos varias veces

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Html, Remainder: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    Html :=
      '<div style="display:flex; gap:12px;">' +
      '  <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Revenue</b><br/>EUR 4,182,300</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Margin</b><br/>18.4%</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Backlog</b><br/>92 days</div>' +
      '</div>';

    Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
    if Remainder <> '' then
      Log('content did not fit - carry the remainder to the next box');

    Lib.SaveToFile('dashboard.pdf');
  finally
    Lib.Free;
  end;
end;

El valor de retorno es la cadena de continuación, que es cómo cada punto de entrada de dibujo de HTML reporta lo que no cupo. Pásala al siguiente cuadro o a la siguiente página y el flujo continúa donde se detuvo

Colocación en grid, y qué puede ser una pista

Las pistas de grid aceptan longitudes fijas, porcentajes, la unidad fr, expresiones simples de repeat() y minmax(). La colocación automática llena la matriz de ocupación de forma determinista, así que el mismo HTML siempre produce la misma disposición. Las coordenadas explícitas pueden superponerse, y eso es deliberado: un diseño que superpone una insignia sobre una tarjeta está expresando una intención, no un error. Cuando solo se da un eje de forma explícita, la colocación busca únicamente en el otro eje

Los elementos que abarcan varias filas aportan su altura medida de vuelta a las filas que cubren, promediada entre ellas, lo cual evita que un elemento alto que abarca varias filas comprima una sola fila mientras deja bajas a sus vecinas:

Html :=
  '<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
  '            gap:10px;">' +
  '  <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
  '  <div>Inspector</div>' +
  '  <div>Date</div>' +
  '  <div style="grid-column:2 / span 2;">Findings summary</div>' +
  '</div>';

Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);

Los hijos de flex y grid se renderizan a través del mismo renderizador de HTML que todo lo demás, que es la propiedad que hace utilizable la función en lugar de convertirla en un mundo aparte. Las fuentes, la cascada CSS, los enlaces, las imágenes, las tablas y otros contenedores flex o grid anidados se comportan dentro de un elemento flex exactamente como lo hacen en el nivel superior, y el plan de diseño externo registra los comandos finales de texto y rectángulo, de modo que dibujar de forma repetida reutiliza la caché de medición existente

¿Por qué las notas al pie son un problema de paginación?

Una nota al pie no es contenido que fluye después del párrafo que contiene su referencia; es contenido que debe aparecer en la parte inferior del mismo cuadro que su referencia. Eso invierte el orden habitual de medición, porque el espacio disponible para el texto del cuerpo ahora depende de contenido que todavía no se ha diseñado

El renderizador, por lo tanto, mide la nota cuando encuentra la referencia, y resta el área de la nota del presupuesto de altura del cuerpo del cuadro acotado actual. Si la referencia, el texto de cuerpo hasta ese punto y la nota no caben todos juntos, el marcador de nota al pie y todo lo que sigue se trasladan juntos a la cadena de continuación. Esa regla es lo que evita los dos fallos clásicos: una nota que se sobreimprime sobre el texto del cuerpo, y una nota varada en una página cuya referencia está en la anterior

En un cuadro acotado, el área de nota se fija en la parte inferior con una regla separadora encima. En la medición sin límites, donde no hay una altura de cuadro a la cual fijarse, el área de nota sigue inmediatamente después del cuerpo. La numeración se lleva en un campo de extensión en la pila de continuación, así que DrawHTMLTextBox y DrawHTMLStory mantienen la secuencia corriendo a través de columnas y páginas, y una cadena de continuación producida antes de que existiera ese campo igualmente se reanuda correctamente

// Las notas al pie dentro de un relato multicolumna mantienen una secuencia continua
Html := LoadTemplate('chapter.html');    // usa marcadores float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // columnas
  16,       // canaleta en puntos
  20,       // páginas máximas para este relato
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Orientación práctica para autores de plantillas

Diseña dentro de los topes documentados. Un contenedor flex con más de 256 hijos directos casi siempre es una tabla de datos disfrazada de flex, y la ruta de tabla la mide mejor de todos modos. Una cuadrícula más grande que 64 por 64 es una hoja de cálculo, y aplica el mismo consejo. Para texto de cuerpo multicolumna, el comportamiento de columnas y guionado descrito en guionado y columnas de texto equilibradas gobierna cómo se ve el flujo dentro de cada columna

Mide antes de dibujar cuando un diseño tiene que ajustarse. GetHTMLTextHeight reporta la altura que necesitaría un ancho dado, que es la forma económica de decidir entre un diseño y otro antes de comprometer tinta. Y trata una cadena de continuación no vacía como algo normal y no excepcional: es el mecanismo por el cual el contenido largo se pagina, no una señal de error

Cuando el HTML proviene de un motor de informes en lugar de plantillas escritas a mano, la ruta guiada por conjunto de datos en el motor de informes de conjunto de datos se combina bien con esto, generando el marcado que flex y grid luego organizan. Y cuando ese mismo contenido también tiene que salir del PDF de nuevo, la ruta de exportación semántica en exportar PDF a Markdown y DOCX cierra el ciclo

El diseño HTML, la generación de informes y la exportación semántica forman parte de una sola biblioteca para Delphi, C++Builder y Free Pascal; la lista completa de funciones está en la página de PDF Library for Delphi