Artículo técnico

Justificación completa para texto PDF en Delphi con HotPDF

La justificación completa es el diseño que hace que una columna de texto se alinee tanto en el borde izquierdo como en el derecho, el aspecto que espera de un libro impreso o de un reporte 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 porque la forma ingenua de medir cada línea convierte una página rápida en una lenta. HotPDF le brinda justificación consciente de la escritura a través de una sola llamada de diseño de caja, y debajo de esa llamada se encuentra una corrección de rendimiento de libro de texto que vale la pena entender por sí misma

Este artículo explica ambos aspectos. Primero, la regla tipográfica que decide cómo se distribuye la holgura para escrituras con espacios entre palabras frente a escrituras sin ellos. Segundo, el cambio de medición que redujo el costo por página de la justificación en aproximadamente ochenta veces sin diferencias visibles en el resultado. Ambos 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 ajustarse

Lo que realmente requiere la justificación completa

Una línea de texto dibujada en su ancho natural casi nunca alcanza el borde derecho de su columna. Siempre hay un resto, la holgura, entre 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 se encuentren con la caja, y la única forma honesta de hacerlo es separando los glifos desde el interior

La regla que separa la buena justificación de la mala es dónde se pone la holgura. Una escritura que redacta palabras con espacios entre ellas, como el inglés y el resto de la familia latina, tiene costuras naturales en cada espacio entre palabras. Ensanchar esos espacios es invisible a la vista porque los lectores ya aceptan que los espacios entre palabras varían. Una escritura que redacta sin espacios entre palabras, como los caracteres Han chinos, el kana japonés o el Hangul coreano, no tiene tales costuras. Allí, la holgura debe distribuirse uniformemente entre glifos adyacentes, que es el principio que los tipógrafos japoneses llaman kintou-waritsuke, espaciado uniforme. Aplicar estiramiento de espacios entre palabras de estilo latino a 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 huecos que marcan un resultado amateur

Cómo decide HotPDF a dónde va el espacio

HotPDF toma esa decisión por cada brecha, no por línea. Cuando justifica una línea, recorre cada par adyacente de glifos y pregunta si hay un límite elástico entre ellos. Un límite es elástico cuando uno de los lados es un espacio o tabulación, el caso latino, o cuando ambos lados son caracteres divisibles CJK, el caso del espaciado uniforme. Cuenta esos límites, divide la holgura de la línea en partes iguales entre ellos y agrega esa parte a cada brecha que cumpla con los requisitos

La consecuencia ocurre naturalmente. Una línea en inglés tiene límites elásticos solo en sus espacios entre palabras, por lo que toda la holgura aterriza allí y las palabras se separan mientras que las letras dentro de cada palabra mantienen su espaciado natural. Una línea Han o kana tiene un límite elástico entre casi cada par de glifos, por lo que la holgura se distribuye uniformemente a lo largo de toda la línea, exactamente el espaciado uniforme entre glifos que requieren esas escrituras. Una línea que es una sola palabra latina larga sin espacio interno no tiene límite elástico en absoluto, por lo que HotPDF la deja en su ancho natural en lugar de despedazar la palabra letra por letra. La misma lógica maneja secuencias mixtas de latín y CJK en una línea sin tener que tratar casos especiales, porque la decisión es local a cada límite

Un límite se excluye deliberadamente en todas partes. La posición después del glifo final de una línea nunca se trata como una brecha, porque estirar allí solo reintroduciría un resto a la derecha, lo cual es lo opuesto a la justificación

Por qué la última línea se deja sola

La última línea de un párrafo es especial, y equivocarse en ella es el error de justificación más común. La última línea de un párrafo suele ser corta, a menudo solo de unas pocas palabras, y estirarla al ancho completo de la columna arrastra esas palabras por toda la página convirtiéndolas en una fila escasa y rota. La tipografía correcta deja la última línea en su ancho natural, alineada a la izquierda

HotPDF detecta la línea final por su posición. A medida que ajusta el texto en líneas, sabe cuándo la línea que acaba de separar llega al final de la cadena provista. Esa línea final se emite con una simple alineación a la izquierda y mantiene su ancho natural. Cada línea anterior a esta se justifica hacia ambos bordes. Los saltos de línea forzados que usted escribe en el texto se respetan tal cual se escribieron, por lo que una línea corta intencional tampoco se estira nunca. El lector ve un bloque de texto rectangular y limpio cuya última línea termina de forma natural, que es lo que espera el ojo

El costo de medición que hacía lenta la justificación

Para justificar una línea, debe conocer su ancho exacto y debe conocer el avance de cada glifo para poder colocar el espacio adicional con precisión. La primera implementación obtenía esos números de la manera obvia. Medía toda la línea con una consulta completa de ancho de Unicode, luego medía prefijo tras prefijo para recuperar el avance de cada glifo mediante la diferencia. Para una línea de N glifos, eso es N+1 llamadas al motor de medición, y cada llamada es un viaje completo de ida y vuelta de GDI, pidiéndole al sistema operativo que forme y mida el texto y devuelva la respuesta

Por línea eso suena barato. A lo largo de una página no lo es. Tome una página A4 densa de texto principal, aproximadamente cuarenta y cinco líneas de unos ochenta caracteres cada una. Con N+1 viajes de ida y vuelta por línea, eso es alrededor de 81 viajes para cada línea y aproximadamente 3,645 para la página, casi todos invertidos en volver a medir el texto que el motor ya había analizado momentos antes. En un trabajo por lotes que produce miles de páginas, esa sobrecarga domina el tiempo de diseño, y cada viaje cruza el límite entre su proceso y el subsistema de gráficos

Una llamada en lugar de N más uno

La solución es el tipo de cambio que parece pequeño y da grandes beneficios. GDI ya puede reportar el ancho total de una cadena y la posición de cada glifo en una sola consulta. HotPDF expone eso a través de GetWideCharAdvances, que llena un arreglo con el avance natural de cada glifo, incluido el espaciado entre letras (kerning), y devuelve el ancho total, en una sola llamada en lugar de N+1. La rutina de justificación, _HPDFEmitJustifiedWideLine internamente, solicita todos los avances a la vez, calcula la holgura, la distribuye a través de los límites elásticos y emite la línea

Para esa misma página A4, la medición por línea se reduce de unos 81 viajes a uno, por lo que la página cae de aproximadamente 3,645 viajes a unos 45, cerca de una reducción de ochenta veces. La salida es idéntica byte a byte, porque nada en la medición cambió excepto cuántas veces se solicita. El mismo motor GDI, las mismas métricas de fuente, el mismo espaciado entre letras alimentan los mismos números. Solo cayó el recuento de viajes de ida y vuelta. Cuando una medición ya es correcta, la optimización adecuada es dejar de solicitarla repetidamente, no aproximarla

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

Una vez que se distribuye la holgura, HotPDF emite la línea con ExtTextOut y un arreglo de avance por glifo, el arreglo Dx. Cada entrada es la distancia desde el origen de un glifo hasta el siguiente, lo cual es el avance natural de ese glifo más su parte de la holgura cuando le sigue un límite elástico. Esto se asigna directamente al modelo de imagen de PDF. El texto posicionado se escribe con el operador TJ, un arreglo que intercala secuencias de glifos con ajustes horizontales explícitos, y los valores Dx se convierten exactamente en esos ajustes. Es por eso que el espacio adicional aterriza entre glifos en posiciones precisas de subpuntos en lugar de ser falsificado con caracteres de relleno, y por qué una línea justificada de HotPDF se mide correctamente si una herramienta posterior la vuelve a leer

Usted no llama a ExtTextOut por su cuenta para párrafos justificados. El punto de entrada es WideTextOutBox, que envuelve una cadena Unicode en una caja y aplica la alineación que solicite. Divide el texto en líneas que se ajustan al ancho de la caja, coloca cada línea hacia abajo según la altura de la caja y devuelve el número de caracteres que logró acomodar antes de quedarse sin espacio vertical. La alineación se elige mediante el enumerador de justificación

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

Los tres primeros se explican por sí solos: alineación a la izquierda, centrada y a la derecha. El cuarto, jtJustify, es la justificación completa en ambos bordes que se describe aquí, y es el valor que lee WideTextOutBox para activar el espaciado consciente de la escritura

Justificando un párrafo en la práctica

Un ejemplo completo crea un documento, establece una fuente y vierte un párrafo en una caja con justificación completa. El mismo código justifica el texto en latín y CJK sin cambiar una bandera, porque el conocimiento de la escritura 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 dibujar el mismo bloque alineado a la izquierda, centrado o alineado a la derecha, cambie solo el argumento final a jtLeft, jtCenter o jtRight. El ajuste de línea, la ubicación de la línea y el valor de retorno siguen siendo los mismos. El ancho medido que impulsa las cuatro rutas proviene de GetWideTextWidth, la consulta de ancho compatible con Unicode que mide correctamente una WideString donde la antigua medición por bytes dimensionaría incorrectamente cualquier cosa más allá de Latin-1, lo cual es lo que hace que la caja ajuste texto CJK y pares subrogados en el lugar correcto para empezar

La justificación es una capa de una pila más grande de conformación de texto. Cuando una línea contiene escrituras que reordenan o unen sus glifos, las decisiones de espaciado aquí se asientan sobre el trabajo descrito en nuestro artículo sobre la conformación de texto de escrituras complejas, y cuando una fuente tiene variantes tipográficas que desea seleccionar, vea cómo controlar las alternativas estilísticas GSUB de OpenType. Todo esto se envía en el Componente HotPDF para Delphi y C++Builder, junto con las APIs más amplias de texto, diseño y documentos cubiertas a lo largo de este blog