HotPDF Component maqueta texto multicol de CSS cuando THPDFHTMLImporter.RenderHTML5 convierte HTML a PDF en Delphi. El motor HTML paginado resuelve column-count y column-width contra la caja de contenido, balancea el último fragmento de cada contenedor multicol con una búsqueda binaria, llena las páginas anteriores columna por columna hasta el fondo de la página, y soporta column-fill, column-span: all y column-rule
La razón habitual para querer esto es aburrida y real: un boletín de producto a dos columnas, un anexo de términos y condiciones compuesto en tres columnas angostas, o un PDF de notas de versión que debería verse como el boletín impreso que el equipo de marketing ya maquetó en HTML. Sin soporte de columnas el importador apila cada párrafo en una medida ancha única, el largo de línea se duplica, y el documento se lee como un archivo de log. Con él, el mismo stylesheet que maneja la vista previa del navegador maneja el PDF, y la parte difícil pasa a ser la paginación, en la que los navegadores casi nunca tienen que pensar en pantalla
¿Cómo decide HotPDF cuántas columnas usar?
HotPDF resuelve el conteo de columnas con las mismas reglas que el pseudoalgoritmo de CSS Multi-column Layout Module Level 1 §3.4, aplicado al ancho de la caja de contenido W después de quitar padding y bordes. Un column-count declarado se toma tal cual. Cuando solo se da column-width, el motor calcula N = Trunc((W + gap) / (column-width + gap)) con un piso de una columna. Cuando ambos están presentes, el resultado es el menor de los dos, así que el conteo declarado puede limitar el conteo derivado pero nunca inflarlo
Un caso concreto: una caja de contenido de 483pt (A4 con márgenes laterales de 56pt, redondeado) y column-width: 140pt; column-gap: 12pt da Trunc(495 / 152) = 3 columnas, cada una de (483 - 2 * 12) / 3 = 153pt de ancho. Agregar column-count: 2 a la misma regla lo baja a dos columnas, porque gana el mínimo. La palabra clave normal para el gap resuelve a 1em del tamaño de fuente del contenedor, y esa resolución ocurre en el momento del layout en lugar del parseo, así que un font-size declarado después de column-gap en la misma regla todavía produce el gap correcto
Una guarda no está en la especificación, y está ahí a propósito. Después de calcular el ancho de columna, cualquier valor por debajo de 12pt colapsa el contenedor de vuelta a una sola columna. Un stylesheet que diga column-count: 9999, sea por accidente o porque vino de una plantilla no confiable, de otro modo produciría miles de columnas con anchos negativos o microscópicos y un divisor de líneas que intenta meter un glifo por línea. Recaer en una columna mantiene la salida legible y el costo de layout acotado. Los conteos cero o negativos nunca llegan tan lejos: el validador acepta solo auto o un entero positivo para column-count, y column-width y column-gap toman auto o normal respectivamente, o un largo en puntos o em
Renderizar una sección HTML multicol a PDF en Delphi
La salida multicol no necesita ningún interruptor en el importador. THPDFHTMLImporter se crea con el documento THotPDF en el que dibuja, BeginDoc ya debe haber corrido, y las propiedades de columna llegan por la cascada normal, sea en el markup o en el stylesheet de autor pasado como segundo argumento de RenderHTML5
uses
SysUtils, HPDFDoc, HPDFHTMLImport;
procedure BuildBulletin(const FileName: string);
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Html, Css: WideString;
begin
Pdf := THotPDF.Create(nil);
Importer := THPDFHTMLImporter.Create(Pdf);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Importer.Margin := 56;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
' column-rule: 0.5pt solid #999999 } ' +
'section.news h2 { column-span: all }';
Html := '<section class="news">' +
'<p>Release 4.2 ships the new export pipeline...</p>' +
'<p>Licensing changes take effect on 1 March...</p>' +
'<h2>Known issues</h2>' +
'<p>Import of legacy archives & templates...</p>' +
'</section>';
if not Importer.RenderHTML5(Html, Css) then
raise Exception.Create('Nothing was emitted');
Pdf.EndDoc;
finally
Importer.Free;
Pdf.Free;
end;
end;
Note el escape doble en el último párrafo: & dentro de la cadena Pascal es la entidad HTML de un ampersand, que el importador decodifica de vuelta a un solo carácter. El resto de la cascada, los presupuestos y los callbacks de recursos se comportan exactamente como se describe en la visión general de la importación de paged-media HTML5 de HotPDF; las columnas son solo un modo de layout más dentro del mismo perfil
Balancear columnas con una búsqueda binaria
HotPDF balancea columnas buscando la altura de columna más pequeña a la que un llenado greedy todavía coloca cada línea restante. La pasada greedy es simple: caminar las líneas hacia la columna uno hasta que la siguiente línea excedería la altura candidata, luego continuar en la columna dos, y así. La búsqueda arranca con la línea individual más alta como cota inferior y la suma de todas las alturas de línea como cota superior, parte a la mitad el intervalo en cada ronda, y se detiene una vez que las cotas están dentro de 0.05pt
Una búsqueda binaria solo funciona si el predicado es monótono, y aquí lo es: una columna más alta nunca puede contener menos líneas que una más baja, así que "cada línea cabe" pasa de falso a verdadero exactamente una vez a medida que la altura crece. Esa propiedad es la que garantiza convergencia y garantiza que el llenado greedy final a la altura encontrada consuma cada línea, en lugar de dejar una línea huérfana que dispararía una página extra espuria. También explica un detalle del resultado: la banda balanceada es tan corta como las cajas de línea permiten, no necesariamente columnas iguales, porque las líneas son indivisibles y la última columna puede terminar una o dos líneas más arriba que las demás
¿Qué pasa cuando las columnas cruzan un salto de página?
Las columnas que no caben en la página actual se llenan secuencialmente hasta el fondo de la página, y solo el fragmento final de página se balancea. Este es el comportamiento que CSS Multi-column Layout Level 1 §7.1 prescribe para column-fill: balance en contextos fragmentados, y es el aspecto de un boletín impreso: columnas llenas en cada página salvo la última, donde el texto restante se reparte parejo en lugar de dejar una columna alta junto a una vacía
Cada fragmento de página pasa por el mismo fragmentador de forma independiente. Si las líneas restantes caben en N columnas de la altura disponible, el fragmento se balancea; de lo contrario cada columna se llena hasta el fondo de la página y el bucle arranca una página nueva. La altura natural del contenedor completo, que el motor usa para decisiones de break-inside: avoid y medición de extensión, es la altura de banda balanceada con capacidad ilimitada, así que un contenedor que cabría en una página fresca como bloque balanceado se mueve ahí intacto en lugar de rebanarse. Cuando una sola línea es más alta que una página entera, el motor relaja la restricción, coloca la línea de todos modos al tope de una página, y reporta un diagnóstico hdkPaginationConstraint con el sujeto multi-column
column-fill: auto y cajas de altura fija
Con column-fill: auto y una height definida, HotPDF llena las columnas en orden, y cada altura de columna es el menor entre el espacio de página restante y la altura del contenedor menos su padding y bordes. El mínimo importa más de lo que parece. Usar solo el espacio de página restante dejaría que la primera columna se trague cada línea siempre que la página tenga más espacio disponible que la caja. Sin una altura definida, column-fill: auto recae en el balanceo, como hacen los navegadores, ya que un contenedor multicol sin restricciones no tiene altura de columna que llenar; la primera implementación pasó por alto ese fallback, y una de las 27 verificaciones de regresión lo atrapó
column-span: all y párrafos anidados
HotPDF soporta column-span: all en hijos directos de un contenedor multicol, siguiendo CSS Multi-column Layout Level 1 §6.1. Un hijo que abarca divide el contenedor en segmentos: los hijos antes de él forman una banda de columnas, el elemento que abarca se renderiza a todo el ancho, y los hijos después de él arrancan una banda nueva. Cada banda se balancea, llena, rige y pagina por su cuenta, así que un encabezado en medio de una sección de tres columnas produce dos bandas balanceadas con un encabezado a todo el ancho entre ellas, que es el layout que la gente realmente dibuja en mockups
Llegar ahí requirió arreglar primero un problema menos visible. Un contenedor como <div style="column-count:2"> con varios hijos <p> renderizaba antes cada párrafo en una sola columna apilada, porque cada párrafo se maquetaba como su propio bloque de texto y la declaración de columnas del padre nunca aplicaba. HotPDF ahora junta el texto de los hijos de bloque anidados en un solo flujo para el contenedor. El recolector tiene que insertar un salto duro en cada frontera de bloque mientras lo hace; saltárselo concatena la última palabra de un párrafo con la primera palabra del siguiente en la misma línea, un bug fácil de perder cuando el texto de prueba termina casualmente en puntuación
Ese flujo fusionado tiene un alcance deliberado. Se usa solo cuando todo el subárbol es flujo de texto plano. Un contenedor que tenga controles de formulario, imágenes, tablas, listas o <hr> en cualquier lugar de su interior conserva la ruta de bloques ordinaria y se renderiza en una sola columna, porque el recolector de texto fusionado no puede expresar su geometría. Las tablas en particular tienen sus propias reglas de paginación, cubiertas en el artículo sobre manejo de rowspan y encabezados de tabla repetidos en HotPDF, y mezclarlas en una banda de columnas rompería esas garantías
Dónde va el column rule, y cómo probarlo
HotPDF dibuja column-rule en la línea media de cada gap, en el borde derecho de una columna más la mitad del gap, desde el tope al fondo de la banda en cada fragmento de página, como describe CSS Multi-column Layout Level 1 §4. El ancho por defecto es 1pt. El color por defecto es el color de texto del elemento cuando no se declara o es currentColor. El shorthand column-rule sin palabra clave de estilo se trata como solid, en línea con cómo comúnmente se lee el shorthand border, mientras que none y hidden suprimen la regla
La geometría de columnas es observable sin parsear el PDF. El evento OnLineLayout de THPDFHTMLImporter se dispara una vez por línea dibujada con un registro THPDFHTMLLineLayout, y como el motor dibuja cada columna por separado, el campo X de cada línea es el borde izquierdo de su columna. Las columnas adyacentes difieren exactamente en el ancho de columna más el gap, lo que hace fácil distinguir un gap de 12pt de uno de 20pt en una prueba de regresión
type
TColumnProbe = class
public
Lines: TStringList;
constructor Create;
destructor Destroy; override;
procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
end;
constructor TColumnProbe.Create;
begin
inherited Create;
Lines := TStringList.Create;
end;
destructor TColumnProbe.Destroy;
begin
Lines.Free;
inherited;
end;
procedure TColumnProbe.LineDrawn(Sender: TObject;
const Line: THPDFHTMLLineLayout);
begin
// PageIndex es base 0; X es el borde izquierdo de la columna de la línea
Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
[Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;
// Cableado, antes de RenderHTML5:
// Probe := TColumnProbe.Create;
// Importer.OnLineLayout := Probe.LineDrawn;
Dos notas prácticas de escribir pruebas contra este evento. PageIndex es base 0, que es lo primero que una aserción escrita a mano suele equivocar. Y X incluye desplazamientos de alineación, así que para texto centrado o alineado a la derecha compare posiciones de columna en fixtures alineados a la izquierda. La regla misma no aparece en el evento; para afirmar su posición, escanee el content stream sin comprimir buscando los operadores verticales m y l
Límites y trampas que vale conocer
Algunos comportamientos son aproximaciones, y es mejor conocerlos antes de una revisión de layout que después. El espaciado de párrafos dentro de un flujo de columnas fusionado se aproxima con una línea de la altura de línea del contenedor en cada frontera de bloque, así que los márgenes por párrafo dentro de una banda de columnas no se reproducen exactos. Cuando column-span: all divide un contenedor, cada segmento repite el padding del contenedor y la caja de fondo, lo que mantiene los segmentos visualmente continuos pero no es una sola caja en el sentido estricto de CSS. Todo valor de column-rule-style distinto de none y hidden pasa la validación, pero la regla se traza como una línea solid
Los valores no soportados no fallan en silencio. Fijar StrictCSS a True convierte cada diagnóstico en una excepción EHPDFHTMLUnsupportedCSS, mientras que OnDiagnostic deja que un trabajo por lotes los registre y siga. Note que el modo estricto aplica también al diagnóstico de paginación, así que un documento con una línea más alta que una página lanzará bajo StrictCSS en lugar de relajar la restricción en silencio
procedure TReportJob.HtmlDiagnostic(Sender: TObject;
const Diagnostic: THPDFHTMLDiagnostic);
begin
case Diagnostic.Kind of
hdkUnsupportedProperty, hdkUnsupportedValue:
FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
hdkPaginationConstraint:
FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
Diagnostic.MessageText);
else
FLog.Add(Diagnostic.MessageText);
end;
end;
// Autoría de plantillas: falle rápido ante cualquier cosa que el motor ignoraría
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;
El modo estricto es la configuración correcta mientras se escribe una plantilla, ya que un error de tipeo como column-span: both se rechaza de inmediato en lugar de dejar en silencio que el encabezado fluya dentro de las columnas. En producción, registrar a través de OnDiagnostic suele ser el mejor intercambio, porque una declaración extraña en HTML provisto por el cliente no debería costar el documento completo. Los diagnósticos idénticos (mismo tipo, sujeto y valor) se reportan una vez mientras quepan en la lista MaxDiagnostics, que por defecto tiene 256 entradas, así que una declaración que la cascada visita para cientos de elementos no inunda el log
FAQ
¿HotPDF soporta el shorthand columns?
Sí. El shorthand columns acepta a lo sumo un conteo entero positivo y un largo de columna en cualquier orden, y auto reinicia ambos longhands. Los valores resueltos siguen las mismas reglas de conteo y ancho que las propiedades individuales
¿Puede un contenedor multicol contener imágenes o tablas?
Puede contenerlos, pero HotPDF entonces renderiza ese contenedor en una sola columna. El flujo de columnas se aplica solo cuando cada descendiente es flujo de texto plano, porque las imágenes, las tablas, las listas y los controles de formulario necesitan su propia geometría de caja y reglas de paginación
¿Por qué la última página muestra columnas balanceadas mientras las anteriores están llenas?
Esa es la regla de CSS para column-fill: balance en medios paginados: solo el fragmento final se balancea. Las páginas anteriores llenan cada columna hasta el fondo de la página para no desperdiciar espacio a mitad del documento
El layout multicol, el perfil de paged-media HTML5 y el resto del pipeline de HTML a PDF son parte del HotPDF Component for Delphi y C++Builder, que también cubre dibujo directo de PDF, formularios, cifrado y extracción de texto