La mise en forme du texte dans le composant PDFium passe par un seul objet installable. ConfigureTextShaper installe le moteur de mise en forme par lequel chaque point d'entrée de mise en forme passe, remplaçant et libérant ce qui s'y trouvait ; ActiveTextShaper renvoie celui installé et crée le défaut de plateforme à la première utilisation ; ActiveTextShaperName rapporte quel backend est actif ; ClearTextShaper abandonne l'installation et laisse le défaut être créé de nouveau. Sous Windows le défaut est TPdfUniscribeTextShaper. Sous Free Pascal il y a TPdfHarfBuzzTextShaper, qui lie libharfbuzz au moment de l'exécution pour qu'une bibliothèque manquante soit une condition rapportée plutôt qu'un échec de chargement
Une interface, deux backends qui divisent le travail de façon complètement différente. Comprendre cette asymétrie est ce qui empêche la voie portable de produire du texte mis en forme correctement mais positionné faussement
Pourquoi le backend Windows est-il une classe et le portable trois morceaux ?
Parce qu'Uniscribe est quatre API faisant semblant d'en être une. ScriptItemize segmente une chaîne par écriture et résout les niveaux bidirectionnels ; ScriptShape mappe les caractères aux glyphes ; ScriptPlace calcule les avancements et les décalages ; ScriptLayout met les exécutions résultantes en ordre visuel. Un backend construit dessus n'a donc plus rien à ajouter, ce qui fait du moteur de mise en forme Windows une seule classe avec une seule méthode
HarfBuzz couvre les deux du milieu. Il met en forme et positionne une exécution dont la direction et l'écriture sont déjà décidées par l'appelant, et il n'a pas d'opinion sur la façon dont un paragraphe se scinde en exécutions ni sur l'ordre dans lequel ces exécutions apparaissent. Le backend portable fournit donc le reste : l'algorithme bidirectionnel résout les niveaux d'imbrication, les fonctions Unicode de HarfBuzz segmentent le texte par écriture, et les exécutions sont disposées dans l'ordre visuel que produit la règle L2 de l'UAX #9. La moitié bidirectionnelle est assez substantielle pour être sa propre unité, décrite dans l'article sur les niveaux d'imbrication UAX #9
Le moteur de mise en forme ne résout pas les polices, et c'est délibéré
Uniscribe lit le binaire de police depuis un contexte de périphérique GDI. Il n'y a pas d'équivalent portable de cela, et en inventer un à l'intérieur d'une unité de mise en forme signifierait décider, au nom de chaque application, si les polices viennent de fontconfig, de CoreText, d'un dossier de polices d'application ou d'une base de données. Le backend HarfBuzz prend donc un résolveur : un callback qui mappe un nom de police aux octets TrueType ou OpenType. Renvoyer False fait échouer la demande de mise en forme de la même façon qu'une police GDI illisible la fait échouer sous Windows
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// Votre politique : fontconfig, CoreText, un dossier de polices d'app, une base de données
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// La propriété passe à l'unité ; appelez une fois au démarrage,
// avant que quoi que ce soit mette du texte en forme
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// Sous Delphi le défaut de plateforme (Uniscribe) est créé à la demande,
// donc aucune installation n'est nécessaire du tout
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
Garder la découverte de polices en dehors du moteur de mise en forme a un second bénéfice qui se manifeste sur les serveurs : le même processus peut mettre en forme avec un jeu de polices embarqué qui n'a rien à voir avec ce qui est installé sur la machine, ce que vous voulez quand la sortie doit être reproductible à l'octet près entre hôtes. Le composant expose aussi un fournisseur de polices du système hôte pour les cas où vous voulez les polices installées, couvert dans l'article sur le fournisseur de polices système
L'enregistrement de résultat est neutre au backend, et les clusters en sont la raison
Les deux backends remplissent le même TPdfShapedText : le texte source, le nom de police, la taille, les octets de police, un tableau d'exécutions, la largeur totale, le compte de glyphes et le compte de caractères logiques. Chaque TPdfShapedRun porte son étendue dans le texte source, sa position X visuelle, sa largeur, son niveau bidirectionnel et un drapeau de droite à gauche, plus ses glyphes. Chaque TPdfShapedGlyph porte un identifiant de glyphe, un avancement, des décalages X et Y, et le cluster auquel il appartient comme un début et une longueur dans le texte source
Ces champs de cluster sont ce qui rend l'enregistrement utilisable plutôt que simplement informatif. La mise en forme n'est pas un mappage un à un : une syllabe devanagari devient un glyphe depuis quatre caractères, une ligature arabe en fusionne deux, et un caractère unique peut produire plusieurs signes diacritiques. Sans étendues de cluster, vous ne pouvez pas placer un caret, tester un clic, ni surligner une sélection, car vous ne pouvez pas dire à quels caractères un glyphe appartient. Avec elles, l'arithmétique est locale et le même code marche pour les deux backends
var
Shaped: TPdfShapedText;
R, G: Integer;
begin
if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
for R := 0 to High(Shaped.Runs) do
begin
// Les exécutions arrivent déjà en ordre visuel avec VisualX rempli
X := Shaped.Runs[R].VisualX;
for G := 0 to High(Shaped.Runs[R].Glyphs) do
begin
EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
X + Shaped.Runs[R].Glyphs[G].OffsetX,
Shaped.Runs[R].Glyphs[G].OffsetY);
X := X + Shaped.Runs[R].Glyphs[G].Advance;
end;
end;
end;
Les budgets appartiennent à l'enregistrement d'options
TPdfTextShapingOptions porte une direction plus trois plafonds : maximum de caractères, maximum de glyphes et maximum d'exécutions, avec une fonction de classe Default qui remplit des valeurs sensées. Les plafonds ne sont pas de la paranoïa face à des entrées malformées ; ce sont de l'arithmétique. La mise en forme dilate : une police avec une substitution contextuelle agressive peut émettre plus de glyphes que de caractères d'entrée, et un paragraphe qui alterne les écritures tous les quelques caractères produit une exécution par bascule. Un document assemblé pour maximiser les deux transforme une chaîne modeste en une grande allocation, et un service qui met en forme du texte depuis des PDF non fiables a besoin d'une limite qu'il a choisie plutôt que d'une limite que la machine impose
Régler la direction explicitement plutôt que de la laisser en automatique vaut la peine chaque fois que vous la connaissez déjà. L'automatique applique les règles de direction de paragraphe pour deviner depuis le premier caractère fort, ce qui est juste pour du texte libre et faux pour un champ de formulaire dont la direction est une propriété du champ plutôt que de la valeur que quelqu'un y a tapée
Liaison à l'exécution, pas une dépendance de construction
Le backend HarfBuzz charge la bibliothèque dynamiquement. C'est une décision de déploiement avec de vraies conséquences : un binaire tourne sur une machine avec HarfBuzz et sur une machine sans, rapportant une capacité réduite dans le second cas au lieu de ne pas démarrer. Pour une bibliothèque livrée à d'autres développeurs, c'est le seul arrangement praticable, car vous ne pouvez pas exiger de chaque consommateur d'un composant PDF qu'il acquière et fasse correspondre en version une bibliothèque de mise en forme dont il peut ne pas avoir besoin
La règle correspondante pour les appelants est de vérifier. ActiveTextShaper renvoie nil quand la plateforme n'a pas de défaut et qu'aucun n'a été configuré, et le point d'entrée de mise en forme le rapporte comme un moteur indisponible plutôt que comme un échec de mise en forme. Ce sont des problèmes différents qui méritent des messages différents : l'un est un écart de déploiement, l'autre est un problème de police ou de texte
Installez une fois, avant que quoi que ce soit mette en forme
L'installation remplace et libère le moteur de mise en forme précédent, donc l'appeler à répétition est sûr mais sans objet, et l'appeler pendant qu'un autre thread met en forme n'est pas du tout sûr. Faites-le au démarrage. Si vous devez retomber sur le défaut de plateforme plus tard, passez nil, ce qui est aussi la façon de défaire un double de test à la fin d'un test
Une fois un backend installé, la mesure et le retour à la ligne se comportent pareil sur les deux plateformes, puisqu'ils consomment les métriques d'exécutions et de glyphes plutôt que d'appeler la plateforme directement ; le modèle de retour à la ligne est décrit dans l'article sur la mesure de texte et le retour à la ligne. Les plateformes et chaînes d'outils prises en charge par le composant sont listées sur la page produit du PDFium Delphi component