Artigo Técnico

PDFlibPas: hifenização e colunas de texto balanceadas

O PDFlibPas flui uma passagem retida por uma a 64 colunas de largura igual com DrawTextFlowColumns, e quebra suas linhas com hifenização limitada e sensível ao idioma assim que você chama SetTextFlowLanguage e SetTextFlowHyphenation. Nove idiomas são suportados, e o idioma pode ser herdado do valor /Lang do Catalog do documento, em vez de definido por flow

Os dois recursos existem pelo mesmo motivo: uma coluna estreita é onde a quebra de linha ingênua deixa de parecer tipografia e passa a parecer um relatório de bug

Por que o texto justificado se desfaz em colunas estreitas?

Porque a justificação distribui o espaço sobrando nos vãos entre palavras de uma linha, e a quantidade sobrando depende do que cabe. Em uma medida larga, o sobressalente é pequeno e o olho nunca percebe. Corte a largura pela metade e uma única palavra longa que não cabe empurra para a linha seguinte, deixando suas antecessoras absorverem todo aquele espaço. Três linhas assim em sequência produzem os canais brancos verticais que os tipógrafos chamam de rios, e que os leitores experimentam como um texto difícil de acompanhar sem saber por quê

A hifenização corrige a causa, e não o sintoma, permitindo uma quebra dentro da palavra. Os compostos do alemão e do holandês tornam isso inegociável: um substantivo de 24 caracteres em uma coluna de 60 milímetros não tem um bom resultado sem um ponto de quebra. O inglês tolera melhor a ausência, e é por isso que produtos pensados primeiro em inglês frequentemente lançam código de layout que quebra na primeira vez em que um cliente alemão o executa

Quais idiomas, e de onde vem o idioma?

A hifenização cobre inglês, alemão, holandês, francês, espanhol, italiano, português, russo e turco. Defina-a explicitamente por flow com SetTextFlowLanguage, ou deixe que seja herdada da entrada /Lang do Catalog do documento, que é o valor que um documento tagueado e acessível já carrega

Vale a pena usar essa herança em vez de sobrescrevê-la. Um documento que declara seu idioma no Catalog está dizendo a mesma coisa, a partir de um único lugar, para leitores de tela, indexadores de busca e hifenização, e um único lugar é onde um fato deveria viver. Se você já está produzindo saída tagueada, como descrito em tagueamento automático para PDFs acessíveis, a entrada de idioma já está definida e o flow pode simplesmente segui-la

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');
      // Ativa, pelo menos 3 caracteres antes da quebra, 3 depois
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // nunca deixa uma única linha órfã

      repeat
        // Três colunas ao longo de uma região de 480 pt, calhas de 18 pt, balanceadas
        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 e MinSuffix são tipografia, não validação

Os dois inteiros depois do flag de ativação definem o número mínimo de caracteres que precisam permanecer antes e depois de uma quebra. Três e três é um padrão conservador que a maioria dos house styles aceita. Dois e dois produz mais oportunidades de quebra e resultados visivelmente mais feios, porque um fragmento de duas letras pendurado no final de uma linha parece um erro de digitação

Aumente os mínimos quando o tipo é grande, onde cada fragmento é visualmente proeminente, e reduza-os apenas quando a coluna é genuinamente estreita e você decidiu que uma medida apertada importa mais do que uma limpa. Essa é uma decisão de house style, não uma decisão técnica, e é exatamente por isso que é um parâmetro, e não uma constante

O que "balanceado" realmente significa aqui?

O parâmetro Balance muda o comportamento apenas no final de uma passagem. Com o balanceamento ativado, as colunas são encurtadas para uma contagem de linhas exatamente igual quando tudo o que resta cabe dentro da região, o que é o que impede que uma página final mostre duas colunas cheias e uma terceira com uma única linha solitária. Quando a passagem não cabe, cada coluna mantém sua altura total, de modo que a página carrega o máximo de texto que puder, e o restante continua na próxima página

Essa assimetria é o padrão correto para documentos contínuos. Balancear no meio de um artigo fluindo desperdiçaria espaço vertical em cada página por um efeito cosmético que ninguém vê, já que as colunas estão cheias de qualquer forma. Balancear no final é onde o olho realmente percebe, e é exatamente ali que ele se aplica

A quebra de linha mede palavras inteiras

O algoritmo de quebra mede palavras completas em vez de acumular larguras de caractere, e reserva uma busca limitada para tokens grandes demais que não cabem em uma linha de forma alguma, como uma URL ou um número de tombamento. Isso mantém o caso comum rápido e o caso patológico limitado, em vez do contrário

Hífens opcionais discricionários e hífens automáticos só são renderizados quando a quebra que eles marcam é a quebra que acaba sendo escolhida. Isso parece óbvio e é um defeito clássico: uma implementação ingênua grava o caractere de hífen durante a medição, e se a quebra se move, o hífen fica para trás no meio de uma linha. Nada parece mais um motor de texto quebrado do que um hífen perdido no meio de uma palavra

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Decida o layout antes de desenhar qualquer coisa
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Linhas que o restante da passagem precisa em uma largura de coluna
    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;

Mantenha as configurações de fonte idênticas entre as caixas

Uma regra rege todo layout baseado em flow, e vale a pena declará-la sem rodeios: DrawTextFlow, DrawTextFlowColumns e MeasureTextFlow todos quebram linhas usando a fonte selecionada no momento em que são chamados. Mude a fonte ou o tamanho entre duas caixas do mesmo flow, ou comece uma nova página sem reselecionar uma, e a segunda caixa quebra de forma diferente do que a primeira mediu

O sintoma é enlouquecedor exatamente porque parece intermitente: texto que cabe na página um transborda na página dois, ou uma contagem de linhas medida discorda do que foi desenhado. Selecione a fonte uma vez antes do loop, reselecione-a depois de cada NewPage, e o flow se comporta. Quando escritas mistas aparecem na mesma passagem, a resolução descrita em fallback automático de fonte para texto CJK e emoji se aplica tanto à medição quanto ao desenho, então as larguras permanecem consistentes também entre as runs de fallback

Para layouts de relatório onde o flow é um elemento entre cabeçalhos, rodapés e blocos orientados a dados, os padrões de composição em o motor de relatórios por dataset se combinam de forma limpa com flows de colunas: meça primeiro, posicione o mobiliário fixo, depois dê ao flow qualquer região que sobrar

O PDFlibPas é uma biblioteca de PDF para Delphi, C++Builder e Lazarus, e o ciclo de vida completo do TextFlow, criação, desenho, medição, inspeção, rewind e liberação, também é exposto por meio das interfaces DLL e ActiveX. A documentação completa está na página da biblioteca PDFlibPas Delphi para PDF