La justification complète est la mise en page qui aligne une colonne de texte à la fois sur les bords gauche et droit, l'aspect que l'on attend d'un livre imprimé ou d'un rapport formel. C'est facile à décrire et étonnamment facile de se tromper, car la réponse à la question "où va l'espace supplémentaire ?" n'est pas la même pour l'anglais que pour le japonais, et parce que la manière naïve de mesurer chaque ligne transforme une page rapide en une page lente. HotPDF vous offre une justification tenant compte du type d'écriture (script-aware) via un seul appel de mise en page en boîte (box-layout), et sous cet appel se trouve une correction de performance classique qui mérite d'être comprise en soi
Cet article aborde ces deux aspects. Premièrement, la règle typographique qui décide de la manière dont l'espace excédentaire (slack) est réparti pour les écritures avec des espaces entre les mots par rapport aux écritures qui n'en ont pas. Deuxièmement, le changement de mesure qui a réduit le coût par page de la justification d'environ quatre-vingts fois sans différence visible dans le résultat. Les deux sont importants si vous générez des documents en volume et souhaitez qu'ils se lisent comme une véritable composition typographique plutôt que comme une sortie à espacement fixe étirée pour s'adapter
Ce qu'exige réellement la justification complète
Une ligne de texte dessinée à sa largeur naturelle n'atteint presque jamais le bord droit de sa colonne. Il y a toujours un reste, le jeu (slack), entre l'endroit où se termine le dernier glyphe et l'endroit où se trouve la limite de la colonne. L'alignement à gauche laisse ce jeu sur la droite. L'alignement à droite le déplace vers la gauche. Le centrage le divise. La justification complète le supprime en élargissant la ligne elle-même jusqu'à ce que les deux bords rencontrent la boîte, et la seule manière honnête de le faire est d'écarter les glyphes de l'intérieur
La règle qui sépare une bonne justification d'une mauvaise est l'endroit où vous placez le jeu. Une écriture qui rédige des mots avec des espaces entre eux, comme l'anglais et le reste de la famille latine, a des jointures naturelles à chaque espace inter-mots. L'élargissement de ces espaces est invisible à l'œil nu car les lecteurs acceptent déjà que les espaces entre les mots varient. Une écriture qui se rédige sans espaces entre les mots, comme les caractères Han chinois, les kana japonais ou le hangeul coréen, n'a pas de telles jointures. Là, le jeu doit être réparti uniformément entre les glyphes adjacents, ce qui est le principe que les typographes japonais appellent kintou-waritsuke, l'espacement uniforme. Appliquer un étirement d'espace inter-mots de style latin sur une ligne CJK, ou entasser tout le jeu dans le seul endroit où une ligne CJK se trouve contenir un espace, produit les lézardes et les vides qui caractérisent les travaux amateurs
Comment HotPDF décide où va l'espace
HotPDF prend cette décision espace par espace, et non ligne par ligne. Lorsqu'il justifie une ligne, il parcourt chaque paire adjacente de glyphes et demande s'il y a une limite extensible entre eux. Une limite est extensible lorsque l'un ou l'autre côté est un espace ou une tabulation, le cas latin, ou lorsque les deux côtés sont des caractères séparables CJK, le cas de l'espacement uniforme. Il compte ces limites, divise le jeu de la ligne de manière égale entre elles et ajoute cette part à chaque espacement éligible
La conséquence en découle naturellement. Une ligne anglaise ne possède des limites extensibles qu'à ses espaces inter-mots, tout le jeu s'y retrouve donc et les mots s'écartent tandis que les lettres à l'intérieur de chaque mot conservent leur espacement naturel. Une ligne Han ou kana possède une limite extensible entre presque chaque paire de glyphes, le jeu se répartit donc uniformément sur toute la ligne, soit exactement l'espacement inter-glyphes régulier que ces écritures exigent. Une ligne qui consiste en un seul long mot latin sans espace interne n'a aucune limite extensible, de sorte que HotPDF la laisse à sa largeur naturelle plutôt que de déchirer le mot lettre par lettre. La même logique gère des séquences mixtes de caractères latins et CJK dans une seule ligne sans traitement d'exception, car la décision est locale à chaque limite
Une limite est délibérément exclue partout. La position après le dernier glyphe d'une ligne n'est jamais traitée comme un espacement, car l'étirement à cet endroit réintroduirait simplement un reste à droite, ce qui est le contraire de la justification
Pourquoi la dernière ligne est laissée tranquille
La dernière ligne d'un paragraphe est spéciale, et s'y tromper est le bogue de justification le plus courant. La dernière ligne d'un paragraphe est généralement courte, souvent seulement quelques mots, et l'étirer sur toute la largeur de la colonne fait glisser ces mots sur la page en une rangée clairsemée et brisée. Une typographie correcte laisse la dernière ligne à sa largeur naturelle, alignée à gauche
HotPDF détecte la ligne de fin par position. Au fur et à mesure qu'il divise le texte en lignes, il sait quand la ligne qu'il vient de détacher atteint la fin de la chaîne fournie. Cette ligne finale est émise avec un simple alignement à gauche et conserve sa largeur naturelle. Chaque ligne qui la précède est justifiée sur les deux bords. Les sauts de ligne forcés (hard line breaks) que vous écrivez dans le texte sont respectés tels qu'ils sont écrits, de sorte qu'une ligne courte intentionnelle n'est jamais étirée non plus. Le lecteur voit un bloc rectangulaire net de texte dont la dernière ligne se termine naturellement, ce que l'œil attend
Le coût de mesure qui rendait la justification lente
Pour justifier une ligne, vous devez connaître sa largeur exacte, et vous devez connaître l'avance de chaque glyphe pour pouvoir placer l'espace supplémentaire précisément. La première implémentation obtenait ces nombres de manière évidente. Elle mesurait la ligne entière avec une requête complète de largeur Unicode, puis mesurait préfixe après préfixe pour récupérer l'avance de chaque glyphe par différence. Pour une ligne de N glyphes, cela représente N+1 appels au moteur de mesure, et chaque appel est un aller-retour GDI complet, demandant au système d'exploitation de mettre en forme et de mesurer le texte et de renvoyer la réponse
Par ligne, cela semble peu coûteux. Sur une page, ça ne l'est pas. Prenez une page A4 dense de texte de corps, soit environ quarante-cinq lignes d'environ quatre-vingts caractères chacune. À N+1 allers-retours par ligne, cela représente environ 81 allers-retours pour chaque ligne et à peu près 3 645 pour la page, dont presque tous passés à remesurer du texte que le moteur avait déjà examiné quelques instants plus tôt. Sur un traitement par lots produisant des milliers de pages, ces frais généraux dominent le temps de mise en page, et chaque aller-retour franchit la frontière entre votre processus et le sous-système graphique
Un seul appel au lieu de N plus un
Le correctif est le genre de changement qui semble mineur et qui rapporte gros. GDI peut déjà renvoyer la largeur totale d'une chaîne et la position de chaque glyphe en une seule requête. HotPDF expose cela via GetWideCharAdvances, qui remplit un tableau avec l'avance naturelle de chaque glyphe, crénage inclus, et renvoie la largeur totale, en un seul appel plutôt que N+1. La routine de justification, _HPDFEmitJustifiedWideLine en interne, demande toutes les avances une fois, calcule le jeu, le répartit sur les limites extensibles et émet la ligne
Pour cette même page A4, la mesure par ligne chute d'environ 81 allers-retours à un seul, la page passe donc d'environ 3 645 allers-retours à environ 45, soit une réduction de près de quatre-vingts fois. La sortie est identique octet pour octet, car rien n'a changé dans la mesure à part le nombre de fois où elle est demandée. Le même moteur GDI, les mêmes métriques de police, le même crénage alimentent les mêmes chiffres. Seul le nombre d'allers-retours a chuté. Lorsqu'une mesure est déjà correcte, la bonne optimisation consiste à cesser de la demander à plusieurs reprises, et non à l'approximer
Comment la ligne atteint la page
Une fois le jeu réparti, HotPDF émet la ligne avec ExtTextOut et un tableau d'avances par glyphe, le tableau Dx. Chaque entrée représente la distance entre l'origine d'un glyphe et le suivant, qui est l'avance naturelle de ce glyphe plus sa part de jeu lorsqu'une limite extensible le suit. Cela correspond directement au modèle d'imagerie PDF. Le texte positionné est écrit avec l'opérateur TJ, un tableau qui intercale les suites de glyphes avec des ajustements horizontaux explicites, et les valeurs Dx deviennent exactement ces ajustements. C'est pourquoi l'espace supplémentaire atterrit entre les glyphes à des positions précises inférieures au point typographique (sub-point) plutôt que d'être truqué avec des caractères de remplissage, et pourquoi une ligne HotPDF justifiée se mesure correctement si un outil en aval la relit
Vous n'appelez pas ExtTextOut vous-même pour les paragraphes justifiés. Le point d'entrée est WideTextOutBox, qui encapsule une chaîne Unicode dans une boîte et applique l'alignement que vous demandez. Il divise le texte en lignes qui correspondent à la largeur de la boîte, place chaque ligne le long de la hauteur de la boîte et renvoie le nombre de caractères qu'il a réussi à insérer avant de manquer d'espace vertical. L'alignement est choisi par l'énumération de justification
type
THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);
Les trois premiers parlent d'eux-mêmes : alignement à gauche, centré et à droite. Le quatrième, jtJustify, est la justification complète sur les deux bords décrite ici, et c'est la valeur que WideTextOutBox lit pour activer l'espacement tenant compte de l'écriture (script-aware)
Justifier un paragraphe en pratique
Un exemple complet crée un document, définit une police et verse un paragraphe dans une boîte avec une justification complète. Le même code justifie le texte latin et CJK sans changement d'indicateur (flag), car la prise en compte de l'écriture se situe en dessous de l'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 :=
'La justification complète répartit le jeu sur chaque ligne remplie de sorte que les ' +
'deux bords rencontrent la colonne, tandis que la dernière ligne conserve sa largeur naturelle. ' +
'Pour les écritures avec des espaces entre les mots, l''espace atterrit entre les mots ; pour ' +
'les écritures sans, il se répartit uniformément entre les glyphes.';
// X, Y, Interligne, LargeurBoite, HauteurBoite, Texte, Alignement
Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Pour dessiner le même bloc aligné à gauche, centré ou aligné à droite, modifiez uniquement le dernier argument en jtLeft, jtCenter ou jtRight. L'habillage, le placement des lignes et la valeur de retour restent les mêmes. La largeur mesurée qui pilote les quatre chemins provient de GetWideTextWidth, la requête de largeur prenant en charge l'Unicode qui mesure un WideString correctement là où l'ancienne mesure au niveau de l'octet dimensionnerait mal tout ce qui dépasse le Latin-1, ce qui permet à la boîte d'envelopper correctement le texte CJK et les paires de substitution (surrogate-pair) dès le départ
La justification est une couche d'une pile de mise en forme de texte plus vaste. Lorsqu'une ligne contient des écritures qui réorganisent ou lient leurs glyphes, les décisions d'espacement s'appuient ici sur le travail décrit dans notre article sur la mise en forme de textes à écritures complexes, et lorsqu'une police comporte des variantes typographiques que vous souhaitez sélectionner, découvrez comment piloter les variantes stylistiques OpenType GSUB. Tout cela est livré dans le Composant HotPDF pour Delphi et C++Builder, aux côtés des API plus larges de texte, de mise en page et de document abordées tout au long de ce blog