Article technique

PDFlibPas : césure et colonnes équilibrées en Delphi

PDFlibPas répartit un passage conservé dans une à 64 colonnes de largeur égale avec DrawTextFlowColumns, et coupe ses lignes avec une césure bornée et consciente de la langue dès que vous appelez SetTextFlowLanguage et SetTextFlowHyphenation. Neuf langues sont prises en charge, et la langue peut être héritée de la valeur /Lang du Catalog du document plutôt que définie par flux

Les deux fonctionnalités existent pour la même raison : une colonne étroite est l'endroit où une coupure de ligne naïve cesse de ressembler à de la composition typographique et commence à ressembler à un rapport de bug

Pourquoi le texte justifié se désagrège-t-il dans les colonnes étroites ?

Parce que la justification répartit l'espace restant dans les espaces entre les mots d'une ligne, et la quantité restante dépend de ce qui tient. Sur une justification large, le reste est faible et l'œil ne le remarque jamais. Divisez la largeur par deux et un seul mot long qui ne tient plus est repoussé à la ligne suivante, laissant ses prédécesseurs absorber tout cet espace. Trois lignes de ce type à la suite produisent les canaux blancs verticaux que les typographes appellent des lézardes, et que les lecteurs perçoivent comme un texte difficile à suivre sans savoir pourquoi

La césure corrige la cause plutôt que le symptôme en autorisant une coupure à l'intérieur du mot. Les mots composés allemands et néerlandais rendent cela incontournable : un substantif de 24 caractères dans une colonne de 60 millimètres n'a aucune issue satisfaisante sans point de coupure. L'anglais tolère mieux l'absence de césure, ce qui explique pourquoi les produits conçus d'abord pour l'anglais livrent souvent un code de mise en page qui s'effondre dès qu'un client allemand l'utilise

Quelles langues, et d'où vient la langue ?

La césure couvre l'anglais, l'allemand, le néerlandais, le français, l'espagnol, l'italien, le portugais, le russe et le turc. Définissez-la explicitement par flux avec SetTextFlowLanguage, ou laissez-la être héritée de l'entrée /Lang du Catalog du document, la valeur qu'un document balisé et accessible porte déjà

Cet héritage mérite d'être exploité plutôt que contourné. Un document qui déclare sa langue dans le Catalog transmet le même fait, depuis un seul endroit, aux lecteurs d'écran, aux indexeurs de recherche et à la césure, et un fait ne devrait vivre qu'à un seul endroit. Si vous produisez déjà une sortie balisée comme décrit dans le balisage automatique pour des PDF accessibles, l'entrée de langue est définie et le flux peut simplement la suivre

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');
      // Activer, au moins 3 caractères avant la coupure, 3 après
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // ne jamais isoler une seule ligne

      repeat
        // Trois colonnes sur une zone de 480 pt, gouttières de 18 pt, équilibrées
        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 et MinSuffix relèvent de la typographie, pas de la validation

Les deux entiers qui suivent l'indicateur d'activation définissent le nombre minimal de caractères devant rester avant et après une coupure. Trois et trois est une valeur par défaut prudente qu'acceptent la plupart des chartes typographiques maison. Deux et deux produit davantage d'occasions de coupure et des résultats nettement plus laids, car un fragment de deux lettres suspendu en fin de ligne se lit comme une faute de frappe

Augmentez les minimums lorsque le corps est grand, car chaque fragment y est visuellement proéminent, et abaissez-les seulement lorsque la colonne est réellement étroite et que vous avez décidé qu'une justification serrée compte plus qu'une justification propre. C'est une décision de charte maison plutôt qu'une décision technique, ce qui explique précisément pourquoi il s'agit d'un paramètre et non d'une constante

Que signifie réellement « équilibré » ici ?

Le paramètre Balance ne change le comportement qu'à la fin d'un passage. Lorsque l'équilibrage est activé, les colonnes sont raccourcies à un nombre de lignes exactement égal quand tout ce qui reste tient dans la zone, ce qui évite qu'une dernière page affiche deux colonnes pleines et une troisième contenant une seule ligne esseulée. Lorsque le passage ne tient pas entièrement, chaque colonne conserve toute sa hauteur afin que la page porte autant de texte que possible, le reste se poursuivant sur la page suivante

Cette asymétrie est le comportement par défaut correct pour les documents continus. Équilibrer au milieu d'un article qui s'écoule gaspillerait de l'espace vertical à chaque page pour un effet cosmétique que personne ne voit, puisque les colonnes sont de toute façon pleines. C'est en fin de passage que l'œil remarque réellement l'effet, et c'est précisément là qu'il s'applique

La coupure de ligne mesure des mots entiers

L'algorithme de coupure mesure des mots complets plutôt que d'accumuler des largeurs de caractères, et réserve une recherche bornée pour les jetons trop grands qui ne tiennent sur aucune ligne, comme une URL ou un numéro d'accession. Cela garde le cas courant rapide et le cas pathologique borné, plutôt que l'inverse

Les traits d'union conditionnels et les traits d'union automatiques ne sont rendus que lorsque la coupure qu'ils marquent est celle finalement retenue. Cela paraît évident et c'est pourtant un défaut classique : une implémentation naïve écrit le caractère de trait d'union pendant la mesure, et si la coupure se déplace, le trait d'union reste au milieu d'une ligne. Rien ne ressemble davantage à un moteur de texte défaillant qu'un trait d'union égaré à l'intérieur d'un mot

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Décider la mise en page avant de dessiner quoi que ce soit
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Lignes dont le reste du passage a besoin sur une largeur de colonne
    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;

Gardez les réglages de police identiques d'une zone à l'autre

Une règle gouverne toute mise en page basée sur les flux et mérite d'être énoncée sans détour : DrawTextFlow, DrawTextFlowColumns et MeasureTextFlow coupent tous les lignes en utilisant la police sélectionnée au moment où ils sont appelés. Changez la police ou la taille entre deux zones du même flux, ou démarrez une nouvelle page sans en resélectionner une, et la seconde zone coupe différemment de ce que la première a mesuré

Le symptôme est exaspérant précisément parce qu'il semble intermittent : un texte qui tenait sur la page un déborde sur la page deux, ou un nombre de lignes mesuré ne correspond pas à ce qui a été dessiné. Sélectionnez la police une fois avant la boucle, resélectionnez-la après chaque NewPage, et le flux se comporte correctement. Quand des écritures mixtes apparaissent dans le même passage, la résolution décrite dans le repli automatique de police pour le texte CJK et emoji s'applique à la fois à la mesure et au dessin, si bien que les largeurs restent cohérentes aussi sur les passages en police de secours

Pour les mises en page de rapports où le flux n'est qu'un élément parmi les en-têtes, pieds de page et blocs pilotés par les données, les modèles de composition de le moteur de rapport par jeu de données se combinent proprement avec les flux en colonnes : mesurez d'abord, placez les éléments fixes, puis confiez au flux la zone restante

PDFlibPas est une bibliothèque PDF pour Delphi, C++Builder et Lazarus, et le cycle de vie complet de TextFlow, création, dessin, mesure, inspection, rembobinage et libération, est également exposé via les interfaces DLL et ActiveX. La documentation complète se trouve sur la page PDFlibPas Delphi PDF library