Artículo Técnico

Justificación completa de texto PDF en Delphi con HotPDF

La justificación completa es el diseño que alinea una columna de texto tanto en el borde izquierdo como en el derecho, el aspecto que se espera de un libro impreso o de un informe formal. Es fácil de describir y sorprendentemente fácil de equivocar, porque la respuesta a la pregunta "¿a dónde va el espacio adicional?" no es la misma para el inglés que para el japonés, y debido a que la forma ingenua de medir cada línea convierte una página rápida en una lenta. HotPDF le brinda una justificación adaptada al tipo de escritura (script-aware) mediante una sola llamada de diseño de caja (box-layout), y debajo de esa llamada se esconde una solución de rendimiento de libro de texto que vale la pena comprender por sí misma

Este artículo repasa ambas cuestiones. En primer lugar, la regla tipográfica que decide cómo se distribuye la holgura en las escrituras con separaciones entre palabras frente a las que no las tienen. En segundo lugar, el cambio de medición que redujo el coste por página de la justificación en aproximadamente ochenta veces sin que haya una diferencia visible en la salida. Ambas importan si genera documentos en volumen y desea que se lean como una composición tipográfica real en lugar de una salida monoespaciada estirada para encajar

Lo que realmente requiere la justificación completa

Una línea de texto dibujada a su anchura natural casi nunca llega al borde derecho de su columna. Siempre queda un remanente, la holgura, entre el lugar donde termina el último glifo y donde se sitúa el límite de la columna. La alineación a la izquierda deja esa holgura a la derecha. La alineación a la derecha la mueve a la izquierda. El centrado la divide. La justificación completa la elimina ensanchando la línea misma hasta que ambos bordes tocan la caja, y la única forma honesta de hacerlo es empujando los glifos para separarlos desde adentro

La regla que separa una buena justificación de una mala es dónde se coloca la holgura. Una escritura que redacta palabras con espacios entre ellas, como el inglés y el resto de la familia latina, tiene uniones naturales en cada espacio entre palabras. Ensanchar esos espacios es invisible al ojo porque los lectores ya aceptan que los huecos entre palabras varíen. Una escritura que redacta sin espacios entre palabras, como los caracteres han chinos, los kana japoneses o el hangul coreano, carece de tales uniones. Allí la holgura debe repartirse uniformemente entre glifos adyacentes, lo cual es el principio que los tipógrafos japoneses denominan kintou-waritsuke (espaciado uniforme). Aplicar un estiramiento del espacio entre palabras de estilo latino en una línea CJK, o meter toda la holgura en el único lugar donde una línea CJK por casualidad contiene un espacio, produce los "ríos" y vacíos que delatan un resultado amateur

Cómo decide HotPDF dónde va el espacio

HotPDF toma esa decisión por hueco, no por línea. Cuando justifica una línea, recorre cada par de glifos adyacentes y se pregunta si hay un límite extensible entre ellos. Un límite es extensible cuando uno de sus lados es un espacio o un tabulador (el caso latino), o cuando ambos lados son caracteres fraccionables CJK (el caso del espaciado uniforme). HotPDF cuenta esos límites, divide la holgura de la línea a partes iguales entre ellos, y añade esa porción a cada hueco elegible

La consecuencia surge de forma natural. Una línea en inglés tiene límites extensibles únicamente en sus espacios entre palabras, de manera que toda la holgura recae allí y las palabras se separan mientras que las letras dentro de cada palabra mantienen su espaciado natural. Una línea en caracteres han o kana tiene un límite extensible entre casi todos los pares de glifos, así que la holgura se distribuye uniformemente a lo largo de toda la línea, logrando exactamente el espaciado uniforme entre glifos que esas escrituras exigen. Una línea que se compone de una sola palabra larga en latín sin espacios internos no posee límite extensible en absoluto, de manera que HotPDF la deja con su anchura natural en vez de despedazar la palabra letra por letra. La misma lógica maneja fragmentos combinados de latín y CJK en una sola línea sin crear casos especiales, debido a que la decisión se toma a nivel local para cada límite

En todas partes se excluye deliberadamente un límite. La posición después del glifo final de una línea nunca se trata como un hueco, dado que estirar allí simplemente reintroduciría un remanente del lado derecho, lo cual es opuesto a la justificación

Por qué la última línea se deja tal cual

La línea final de un párrafo es especial, y cometer un error en ella es el fallo de justificación más frecuente. La última línea de un párrafo suele ser corta, a menudo de unas pocas palabras, y estirarla hasta ocupar todo el ancho de la columna arrastra esas palabras por toda la página, convirtiéndola en una fila dispersa y rota. La tipografía correcta deja la última línea con su anchura natural, alineada a la izquierda

HotPDF detecta la línea de cierre por su posición. Al acomodar el texto en líneas, sabe cuándo la línea que acaba de dividir alcanza el final de la cadena provista. Esa línea final se emite con una alineación izquierda simple y conserva su anchura natural. Cada línea que la precede se justifica hacia ambos bordes. Los saltos de línea manuales que escriba en el texto se respetan tal y como fueron escritos, por lo que una línea corta intencionada tampoco se estira nunca. El lector aprecia un bloque de texto rectangular limpio cuya última línea finaliza con naturalidad, lo cual es lo que el ojo espera

El costo de medición que hizo lenta la justificación

Para justificar una línea se debe conocer su anchura exacta y el avance de cada glifo, a fin de situar el espacio adicional con precisión. La primera implementación obtenía estos números de forma obvia. Medía la línea completa con una consulta de anchura Unicode íntegra, y luego medía prefijo tras prefijo para calcular el avance de cada glifo por diferencias. Para una línea de N glifos eso significa N+1 llamadas al motor de medición, y cada llamada es un viaje completo de ida y vuelta a GDI, en el que se le pide al sistema operativo que modele y mida el texto para luego devolver la respuesta

Por línea suena a poco coste. A lo largo de una página no lo es. Considere una página A4 densa de texto de cuerpo, aproximadamente cuarenta y cinco líneas de unos ochenta caracteres cada una. Con N+1 viajes de ida y vuelta por línea, hablamos de unos 81 viajes para cada línea y aproximadamente 3.645 para la página, en su gran mayoría invertidos en remedir texto que el motor ya había inspeccionado instantes atrás. En un trabajo por lotes que produzca miles de páginas, esa sobrecarga domina el tiempo de diseño, y cada viaje de ida y vuelta cruza la frontera entre su proceso y el subsistema de gráficos

Una llamada en vez de N más una

La solución es esa clase de cambio que parece menor pero rinde enormemente. GDI ya es capaz de notificar la anchura total de una cadena y la posición de cada glifo en una única consulta. HotPDF lo expone a través de GetWideCharAdvances, que llena un array con el avance natural de cada glifo (interletraje incluido) y devuelve el ancho total en una sola llamada en vez de N+1. La rutina de justificación (internamente _HPDFEmitJustifiedWideLine) pide todos los avances de una vez, calcula la holgura, la reparte a lo largo de los límites extensibles y emite la línea

Para esa misma página A4 la medición por línea se reduce de 81 viajes a uno, con lo cual la página cae de cerca de 3.645 viajes de ida y vuelta a tan solo 45 (una rebaja casi de 80 veces). La salida es idéntica byte a byte, ya que nada en la medición ha cambiado aparte del número de veces que se solicita. El mismo motor GDI, las mismas métricas tipográficas y el mismo interletraje alimentan los mismos números. Lo único que bajó fue el recuento de viajes de ida y vuelta. Cuando una medición ya es correcta, la optimización apropiada consiste en dejar de solicitarla repetidamente, no en aproximarla

Cómo llega la línea a la página

Una vez que se reparte la holgura, HotPDF emite la línea mediante ExtTextOut y un array de avance por glifo, el array Dx. Cada entrada representa la distancia entre el origen de un glifo y el siguiente, que es el avance natural de ese glifo sumado a su cuota de la holgura cuando le sigue un límite extensible. Esto se traduce directamente en el modelo de imágenes (imaging model) del PDF. El texto posicionado se escribe empleando el operador TJ, un array que intercala series de glifos con ajustes horizontales explícitos, y los valores de Dx se convierten exactamente en esos ajustes. Esa es la razón de que el espacio sobrante aterrice entre los glifos en precisas ubicaciones sub-punto en lugar de fingirse con caracteres de relleno, y de por qué una línea justificada en HotPDF arroja una medida correcta si una herramienta de la fase posterior (downstream) la vuelve a leer

Usted no llama a ExtTextOut por sí mismo para párrafos justificados. El punto de entrada es WideTextOutBox, el cual envuelve una cadena Unicode en una caja y le aplica la alineación solicitada. Esta divide el texto en líneas que encajen en la anchura de la caja, coloca cada línea a lo largo de la altura de la caja y devuelve el número de caracteres que logró acomodar antes de quedarse sin margen vertical. La alineación se escoge por medio de la enumeración (enum) de justificación

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

Las primeras tres opciones se explican por sí solas: alineación izquierda, centrada y derecha. La cuarta, jtJustify, es la justificación completa hacia ambos bordes aquí descrita, y es el valor que lee WideTextOutBox para activar el espaciado adaptado al script

Justificando un párrafo en la práctica

Un ejemplo completo crea un documento, configura una fuente y vierte un párrafo dentro de una caja con justificación completa. El mismo código justifica texto latino y CJK sin cambiar de bandera, dado que la adaptación al script reside por debajo de la API

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Full justification spreads the slack on each filled line so both ' +
      'edges meet the column, while the last line keeps its natural width. ' +
      'For scripts with word gaps the space lands between words; for ' +
      'scripts without them it spreads evenly between glyphs.';

    // X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Para trazar el mismo bloque alineado a la izquierda, centrado o a la derecha, tan solo cambie el argumento final a jtLeft, jtCenter o jtRight. El empaquetamiento (wrapping), la colocación de la línea y el valor retornado continúan siendo los mismos. La anchura medida que rige las cuatro vías proviene de GetWideTextWidth, la consulta de anchura consciente de Unicode que computa de forma precisa un WideString (allí donde una medida anterior orientada a bytes erraría de tamaño para todo lo posterior a Latin-1), la cual es precisamente lo que hace que la caja ajuste texto CJK y pares sustitutos en el sitio correcto desde el inicio

La justificación es una capa de una pila de modelado de texto mayor. Cuando una línea contiene escrituras que reordenan o juntan sus glifos, las determinaciones de espaciado aquí citadas se basan en la labor explicada en nuestro artículo sobre el modelado de texto para scripts complejos, y cuando una fuente posee variaciones tipográficas que desea elegir, consulte cómo gestionar las alternativas estilísticas de OpenType GSUB. Todo ello se proporciona en el Componente HotPDF para Delphi y C++Builder, sumado a un rango mayor de API para texto, diseño (layout) y documentos que se cubren a lo largo de este blog