Technical Article

Giustificazione completa per il testo PDF in Delphi con HotPDF

La giustificazione completa è il layout che allinea una colonna di testo sia sul margine sinistro sia su quello destro, l'aspetto che ci si aspetta da un libro stampato o da un rapporto formale. È facile da descrivere e sorprendentemente facile da sbagliare, perché la risposta alla domanda "dove va a finire lo spazio extra" non è la stessa per l'inglese come per il giapponese, e perché il modo ingenuo di misurare ogni riga trasforma una pagina veloce in una lenta. HotPDF offre una giustificazione consapevole dello script tramite una singola chiamata di layout a riquadro, e alla base di questa chiamata vi è una correzione delle prestazioni da manuale che vale la pena comprendere a sé stante

Questo articolo li esamina entrambi. In primo luogo, la regola tipografica che decide come viene distribuito lo spazio in eccesso (slack) per gli script con spazi tra le parole rispetto agli script senza. In secondo luogo, la modifica alla misurazione che ha ridotto il costo della giustificazione per pagina di circa ottanta volte senza alcuna differenza visibile nell'output. Entrambi contano se si generano documenti in grandi volumi e si desidera che risultino impaginati professionalmente anziché apparire come output a spaziatura fissa stirato per adattarsi

Cosa richiede effettivamente la giustificazione completa

Una riga di testo disegnata alla sua larghezza naturale non raggiunge quasi mai il margine destro della sua colonna. C'è sempre un resto, lo slack, tra il punto in cui termina l'ultimo glifo e il limite della colonna. L'allineamento a sinistra lascia questo slack sulla destra. L'allineamento a destra lo sposta a sinistra. La centratura lo divide. La giustificazione completa lo rimuove allargando la riga stessa finché entrambi i bordi non toccano il riquadro, e l'unico modo onesto per farlo è allontanare i glifi dall'interno

La regola che separa una buona giustificazione da una cattiva è dove si posiziona lo slack. Uno script che scrive parole con spazi tra di esse, come l'inglese e il resto della famiglia latina, ha cuciture naturali a ogni spazio inter-parola. Allargare quegli spazi è invisibile all'occhio perché i lettori accettano già che la distanza tra le parole vari. Uno script che scrive senza spazi, come i caratteri Han cinesi, i kana giapponesi o l'Hangul coreano, non ha tali cuciture. Lì lo slack deve essere distribuito uniformemente tra glifi adiacenti, che è il principio che i tipografi giapponesi chiamano kintou-waritsuke, spaziatura uniforme. Applicare uno stiramento degli spazi inter-parola in stile latino su una riga CJK, o stipare tutto lo slack nell'unico posto in cui una riga CJK capita di contenere uno spazio, produce i fiumi e gli spazi vuoti che contraddistinguono un output amatoriale

Come HotPDF decide dove va lo spazio

HotPDF prende questa decisione per ogni interruzione (gap), non per riga. Quando giustifica una riga percorre ogni coppia adiacente di glifi e si chiede se tra loro ci sia un confine estensibile. Un confine è estensibile quando uno dei lati è uno spazio o una tabulazione, il caso latino, o quando entrambi i lati sono caratteri CJK separabili, il caso della spaziatura uniforme. Conta tali confini, divide lo slack della riga in parti uguali tra di essi e aggiunge quella quota a ciascun gap qualificato

La conseguenza si manifesta in modo naturale. Una riga inglese ha confini estensibili solo negli spazi tra le parole, perciò tutto lo slack finisce lì e le parole si distanziano mentre le lettere all'interno di ciascuna parola mantengono la loro spaziatura naturale. Una riga Han o kana ha un confine estensibile tra quasi ogni coppia di glifi, quindi lo slack si distribuisce uniformemente lungo l'intera riga, esattamente la spaziatura inter-glifo uniforme che tali script richiedono. Una riga composta da un'unica lunga parola latina senza spazi interni non ha alcun confine estensibile, perciò HotPDF la lascia alla sua larghezza naturale anziché dilaniare la parola lettera per lettera. La stessa logica gestisce sequenze miste di latino e CJK in una singola riga senza casi speciali, poiché la decisione è locale per ciascun confine

Un confine è deliberatamente escluso ovunque. La posizione dopo l'ultimo glifo di una riga non è mai trattata come un gap, perché l'estensione in quel punto reintrodurrebbe semplicemente un resto a destra, che è l'opposto della giustificazione

Perché l'ultima riga viene lasciata inalterata

L'ultima riga di un paragrafo è speciale, e sbagliarla è il bug di giustificazione più comune. L'ultima riga di un paragrafo è solitamente breve, spesso solo di poche parole, e stirarla per tutta la larghezza della colonna trascina quelle parole attraverso la pagina creando una fila sparsa e frammentata. La tipografia corretta lascia l'ultima riga alla sua larghezza naturale, allineata a sinistra

HotPDF rileva la riga finale in base alla posizione. Mentre manda a capo il testo in righe, sa quando la riga appena divisa raggiunge la fine della stringa fornita. Quella riga finale viene emessa con un semplice allineamento a sinistra e mantiene la sua larghezza naturale. Ogni riga precedente è giustificata su entrambi i bordi. Le interruzioni di riga rigide inserite nel testo vengono onorate così come sono scritte, perciò anche una riga breve intenzionale non viene mai allungata. Il lettore vede un blocco rettangolare di testo pulito la cui ultima riga termina in modo naturale, che è ciò che l'occhio si aspetta

Il costo di misurazione che rendeva lenta la giustificazione

Per giustificare una riga bisogna conoscere la sua larghezza esatta e si deve conoscere l'avanzamento (advance) di ogni glifo per posizionare lo spazio extra con precisione. La prima implementazione otteneva questi numeri nel modo più ovvio. Misurava l'intera riga con un'interrogazione Unicode completa della larghezza, poi misurava prefisso dopo prefisso per recuperare l'avanzamento di ciascun glifo per differenza. Per una riga di N glifi questo significa N+1 chiamate al motore di misurazione, e ogni chiamata è un viaggio di andata e ritorno GDI completo, in cui si chiede al sistema operativo di formare e misurare il testo e restituire la risposta

Per singola riga questo sembra economico. Su un'intera pagina non lo è. Si consideri una fitta pagina A4 di testo, all'incirca quarantacinque righe di circa ottanta caratteri ciascuna. A N+1 viaggi per riga si tratta di circa 81 chiamate per ogni riga e circa 3.645 per la pagina, quasi tutte impiegate a rimisurare un testo che il motore aveva già esaminato pochi istanti prima. In un processo batch che produce migliaia di pagine, questo sovraccarico domina il tempo di layout, e ogni viaggio attraversa il confine tra il processo e il sottosistema grafico

Una sola chiamata anziché N più uno

La soluzione è il tipo di modifica che sembra piccola ma offre un grande risultato. GDI può già segnalare la larghezza totale di una stringa e la posizione di ogni glifo in una singola interrogazione. HotPDF espone questa funzionalità tramite GetWideCharAdvances, che riempie un array con l'avanzamento naturale di ciascun glifo, crenatura (kerning) inclusa, e restituisce la larghezza totale, in un'unica chiamata anziché N+1. La routine di giustificazione, internamente _HPDFEmitJustifiedWideLine, richiede tutti gli avanzamenti in una volta sola, calcola lo slack, lo distribuisce attraverso i confini estensibili ed emette la riga

Per quella stessa pagina A4, la misurazione per riga scende da circa 81 chiamate a una sola, cosicché l'intera pagina passa da circa 3.645 chiamate a circa 45, ovvero una riduzione di quasi ottanta volte. L'output è identico byte per byte, poiché niente della misurazione è cambiato tranne il numero di volte in cui viene richiesta. Lo stesso motore GDI, le stesse metriche dei font e la stessa crenatura forniscono gli stessi numeri. Solo il conteggio delle chiamate è sceso. Quando una misurazione è già corretta, l'ottimizzazione giusta consiste nello smettere di richiederla ripetutamente, non nell'approssimarla

Come la riga arriva sulla pagina

Una volta ripartito lo slack, HotPDF emette la riga con ExtTextOut e un array di avanzamento per ogni glifo, l'array Dx. Ogni voce rappresenta la distanza dall'origine di un glifo a quella successiva, che corrisponde all'avanzamento naturale del glifo più la sua quota di slack quando è seguito da un confine estensibile. Questo si mappa direttamente sul modello di imaging del PDF. Il testo posizionato viene scritto con l'operatore TJ, un array che alterna sequenze di glifi con regolazioni orizzontali esplicite, e i valori Dx diventano esattamente quelle regolazioni. Questo è il motivo per cui lo spazio extra finisce tra i glifi in posizioni sub-punto precise anziché essere simulato con caratteri di riempimento, e perché una riga giustificata in HotPDF si misura correttamente qualora uno strumento a valle la rilegga

Non si chiama direttamente ExtTextOut per i paragrafi giustificati. Il punto di ingresso è WideTextOutBox, che avvolge una stringa Unicode in un riquadro e applica l'allineamento richiesto. Divide il testo in righe che si adattano alla larghezza del riquadro, posiziona ogni riga lungo l'altezza del riquadro e restituisce il numero di caratteri che è riuscito a inserire prima di esaurire lo spazio verticale. L'allineamento è scelto dall'enumerazione della giustificazione

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

I primi tre sono i chiari allineamenti a sinistra, al centro e a destra. Il quarto, jtJustify, è la giustificazione completa su entrambi i margini descritta qui, ed è il valore che WideTextOutBox legge per attivare la spaziatura consapevole dello script

Giustificare un paragrafo nella pratica

Un esempio completo crea un documento, imposta un font e riversa un paragrafo in un riquadro con giustificazione completa. Lo stesso codice giustifica il testo latino e CJK senza alcun cambio di flag, perché la consapevolezza dello script vive al di sotto dell'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;

Per disegnare lo stesso blocco allineato a sinistra, al centro o a destra, basta modificare solo l'argomento finale in jtLeft, jtCenter o jtRight. Il ritorno a capo, il posizionamento delle righe e il valore restituito rimangono invariati. La larghezza misurata che guida tutti e quattro i percorsi proviene da GetWideTextWidth, l'interrogazione della larghezza consapevole di Unicode che misura correttamente una WideString laddove la vecchia misurazione basata sui byte calcolerebbe male qualsiasi cosa oltre Latin-1, che è ciò che permette al riquadro di mandare a capo il testo CJK e le coppie surrogate nel posto giusto fin dall'inizio

La giustificazione è uno strato di uno stack di modellazione del testo più ampio. Quando una riga contiene script che riordinano o uniscono i loro glifi, le decisioni di spaziatura descritte qui si basano sul lavoro illustrato nel nostro articolo sulla modellazione del testo per script complessi, e quando un font possiede varianti tipografiche che si desidera selezionare, si veda come pilotare le alternative stilistiche OpenType GSUB. Tutto ciò viene fornito con HotPDF Component per Delphi e C++Builder, insieme alle più ampie API di testo, layout e documento trattate in questo blog