Artículo técnico

Guionado y columnas de texto equilibradas en PDFlibPas

PDFlibPas hace fluir un pasaje retenido por entre una y 64 columnas de igual ancho con DrawTextFlowColumns, y corta sus líneas con un guionado acotado y consciente del idioma en cuanto se llama a SetTextFlowLanguage y SetTextFlowHyphenation. Hay nueve idiomas soportados, y el idioma puede heredarse del valor /Lang del Catalog del documento en lugar de fijarse por cada flujo

Ambas funciones existen por la misma razón: una columna estrecha es el punto donde un corte de línea ingenuo deja de parecer composición tipográfica y empieza a parecer un parte de fallos

¿Por qué se desmorona el texto justificado en columnas estrechas?

Porque la justificación reparte el espacio sobrante entre los huecos de palabra de una línea, y la cantidad sobrante depende de lo que quepa. En una medida ancha, lo sobrante es poco y el ojo nunca lo nota. Reduzca el ancho a la mitad y una sola palabra larga que no cabe se empuja a la línea siguiente, dejando a las anteriores absorber todo ese espacio. Tres líneas así seguidas producen los canales blancos verticales que los tipógrafos llaman ríos, y que el lector percibe como un texto difícil de seguir sin saber por qué

El guionado corrige la causa en lugar del síntoma, permitiendo un corte dentro de la palabra. Los compuestos del alemán y del neerlandés hacen esto innegociable: un sustantivo de 24 caracteres en una columna de 60 milímetros no tiene buen resultado posible sin un punto de corte. El inglés tolera mejor su ausencia, y por eso los productos pensados primero para inglés suelen llevar código de maquetación que se cae en cuanto lo usa un cliente alemán

¿Qué idiomas hay, y de dónde viene el idioma?

El guionado cubre inglés, alemán, neerlandés, francés, español, italiano, portugués, ruso y turco. Se fija explícitamente por flujo con SetTextFlowLanguage, o se hereda de la entrada /Lang del Catalog del documento, que es el valor que ya lleva un documento etiquetado y accesible

Esa herencia merece usarse en lugar de sobrescribirse. Un documento que declara su idioma en el Catalog le está diciendo el mismo hecho, desde un único lugar, a los lectores de pantalla, a los indexadores de búsqueda y al guionado, y un único lugar es donde debe residir un hecho. Si ya está generando salida etiquetada como se describe en el etiquetado automático para PDFs accesibles, la entrada de idioma ya está fijada y el flujo puede simplemente seguirla

var
  Lib: TPDFlib;
  Flow, Drawn: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('Georgia', 1);
    Lib.SetTextSize(10.5);

    Flow := Lib.NewTextFlow(ArticleBody);
    try
      Lib.SetTextFlowLanguage(Flow, 'de');
      // Activar, al menos 3 caracteres antes del corte, 3 después
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // nunca dejar una sola línea aislada

      repeat
        // Tres columnas en una región de 480 pt, calles de 18 pt, equilibrado
        Drawn := Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
        if (Drawn = 0) or (Lib.TextFlowFinished(Flow) = 1) then
          Break;
        Lib.NewPage;
      until False;
    finally
      Lib.ReleaseTextFlow(Flow);
    end;

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

MinPrefix y MinSuffix son tipografía, no validación

Los dos enteros que siguen al indicador de activación fijan el número mínimo de caracteres que deben quedar antes y después de un corte. Tres y tres es un valor por defecto conservador que aceptan la mayoría de los manuales de estilo. Dos y dos produce más oportunidades de corte y resultados notablemente más feos, porque un fragmento de dos letras colgando al final de una línea se lee como una errata

Suba los mínimos cuando el cuerpo de letra sea grande, donde cada fragmento resulta visualmente prominente, y bájelos solo cuando la columna sea genuinamente estrecha y haya decidido que una medida ajustada importa más que una limpia. Es una decisión de estilo editorial, no técnica, y por eso es un parámetro y no una constante

¿Qué significa "equilibrado" aquí realmente?

El parámetro Balance solo cambia el comportamiento al final de un pasaje. Con el equilibrado activado, las columnas se acortan a un número exacto de líneas iguales cuando todo lo que queda cabe dentro de la región, lo que evita que una última página muestre dos columnas completas y una tercera con una única línea solitaria. Cuando el pasaje no cabe, cada columna conserva toda su altura para que la página lleve todo el texto que pueda, y el resto continúa en la página siguiente

Esa asimetría es el comportamiento correcto por defecto para documentos continuos. Equilibrar en mitad de un artículo fluido desperdiciaría espacio vertical en cada página por un efecto cosmético que nadie ve, ya que las columnas están de todos modos llenas. Equilibrar al final es donde el ojo realmente lo nota, y es precisamente ahí donde se aplica

El corte de línea mide palabras completas

El algoritmo de corte mide palabras completas en lugar de acumular anchos de carácter, y reserva una búsqueda acotada para los tokens sobredimensionados que no caben en ninguna línea, como una URL o un número de expediente. Eso mantiene rápido el caso común y acotado el caso patológico, en lugar de al revés

Los guiones discrecionales y los guiones automáticos solo se dibujan cuando el corte que marcan es el que finalmente se elige. Esto suena obvio y es un defecto clásico: una implementación ingenua escribe el carácter de guion durante la medición, y si el corte se mueve, el guion se queda atrás en mitad de una línea. Nada delata más un motor de texto averiado que un guion perdido en medio de una palabra

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Decidir la maquetación antes de dibujar nada
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Líneas que necesita el resto del pasaje a un ancho de columna
    Needed := Lib.MeasureTextFlow(Flow, 148);
    if Needed > 3 * LinesPerColumn then
      UseTwoPageSpread
    else
      UseSinglePage;

    Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);

    if Lib.TextFlowFinished(Flow) <> 1 then
      CarryOver(Lib.GetTextFlowRemaining(Flow));
  finally
    Lib.ReleaseTextFlow(Flow);
  end;
end;

Mantenga la configuración de fuente idéntica entre cajas

Una regla gobierna toda maquetación basada en flujo y merece decirse sin rodeos: DrawTextFlow, DrawTextFlowColumns y MeasureTextFlow cortan todas las líneas usando la fuente seleccionada en el momento en que se invocan. Cambie la fuente o el tamaño entre dos cajas del mismo flujo, o inicie una página nueva sin volver a seleccionar una, y la segunda caja se cortará de forma distinta a lo que midió la primera

El síntoma resulta exasperante precisamente porque parece intermitente: un texto que cabe en la página uno desborda en la página dos, o un recuento de líneas medido no coincide con lo que se dibujó. Seleccione la fuente una vez antes del bucle, vuelva a seleccionarla después de cada NewPage, y el flujo se comporta como debe. Cuando aparecen escrituras mixtas en el mismo pasaje, la resolución descrita en la sustitución automática de fuentes para CJK y emoji se aplica tanto a la medición como al dibujo, así que los anchos siguen siendo coherentes también en los tramos de sustitución

Para maquetas de informe donde el flujo es un elemento más junto a cabeceras, pies y bloques con datos, los patrones de composición de el motor de informes basado en dataset combinan bien con los flujos de columnas: mida primero, coloque los elementos fijos y luego dé al flujo la región que quede

PDFlibPas es una biblioteca PDF para Delphi, C++Builder y Lazarus, y todo el ciclo de vida de TextFlow, creación, dibujo, medición, inspección, rebobinado y liberación, también se expone a través de las interfaces DLL y ActiveX. La documentación completa está en la página de PDFlibPas para Delphi