L'appel qui place du texte sur une page PDF est simple. Vous donnez à AddText une chaîne, une police, une taille et une position, et les glyphes apparaissent. Ce qu'il ne fait pas, c'est de vous dire quelle sera la largeur de cette chaîne une fois qu'elle sera dessinée, et il ne coupe pas une longue chaîne sur plusieurs lignes. Un seul appel peint une suite de texte à une position. Si la suite est plus large que la colonne dans laquelle vous vouliez qu'elle s'insère, elle dépasse simplement le bord, et rien dans l'appel de dessin ne vous en avertit. Au moment où vous souhaitez un paragraphe plutôt qu'une simple étiquette, la pièce manquante est la largeur d'une chaîne dans la police et la taille choisies, mesurée avant de la valider sur la page
C'est le problème classique de mise en page. Pour envelopper un paragraphe dans une colonne, vous devez savoir, mot par mot, combien d'espace horizontal chaque ligne candidate prendra, et vous devez le savoir avant de dessiner quoi que ce soit. Le retour à la ligne (word wrap) est une boucle de mesure enroulée autour d'un appel de dessin, et une liaison qui ne fait que dessiner vous donne la seconde moitié. La prise en charge de la mesure de texte dans le composant PDFium comble cette lacune avec deux fonctions, MeasureText et MeasureTextWidth, qui signalent l'étendue rendue d'une chaîne sans placer de marque sur aucune page
Pourquoi la mesure est un class helper, pas une nouvelle méthode sur TPdf
La prise en charge de la mesure arrive sous la forme d'un assistant de classe (class helper) Delphi pour TPdf, résidant dans sa propre unité, plutôt que comme de nouvelles méthodes boulonnées dans la classe TPdf. Un assistant de classe est une fonctionnalité de langage qui vous permet d'attacher des méthodes à un type existant depuis l'extérieur de sa déclaration. Une fois que l'unité est dans la portée, les nouvelles méthodes sont appelées exactement comme si elles appartenaient à la classe, de sorte qu'une méthode d'assistance se lit comme Pdf.MeasureTextWidth(...) sans aucun objet distinct à construire ou à transmettre
La raison de cette stratification est la séparation. Le type de base TPdf reste tel quel, sans champ ajouté et sans signature existante touchée, de sorte qu'un projet qui n'a jamais besoin de mise en page ne transporte jamais le code de mesure. Un projet qui en a besoin ajoute une unité à une clause uses et les méthodes s'allument. La capacité devient opt-in à la granularité d'une seule unité, ce qui est la façon la plus propre d'étendre un type que vous ne possédez pas ou que vous ne voulez pas perturber
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
La mesure doit être exempte d'effets secondaires. Elle doit signaler une largeur sans rien laisser derrière elle, car vous l'appelez de nombreuses fois tout en décidant d'une mise en page et la page doit ressembler exactement à ce qu'elle aurait été si vous n'aviez jamais mesuré du tout. La technique qui rend cela possible consiste à construire un objet texte, à lui demander sa taille et à le jeter avant qu'il ne soit jamais attaché à une page
La séquence comprend quatre appels PDFium. FPDFPageObj_NewTextObj crée un objet texte contre le document, compte tenu du nom et de la taille de la police. FPDFText_SetText définit la chaîne que cet objet transporte. FPDFPageObj_GetBounds lit la boîte englobante de l'objet. FPDFPageObj_Destroy libère l'objet. Surtout, rien dans cette séquence n'appelle l'API d'insertion de page. L'objet est créé, interrogé et détruit de manière isolée, de sorte que le document reste inchangé lorsque la fonction est renvoyée. C'est une sonde jetable dont la seule sortie sont les quatre nombres de sa boîte englobante
C'est la méthode robuste de le faire, car PDFium n'expose pas de largeur d'avance pratique par glyphe que vous pourriez additionner vous-même. Les métriques des glyphes dépendent du programme de police, de l'encodage et de la manière dont PDFium charge la face, et il n'y a pas d'appel public qui vous donne l'avance de chaque caractère d'une chaîne. La boîte englobante d'un objet texte réel, en revanche, est calculée par le même mécanisme qui mettrait en place les glyphes pour le dessin, elle reflète donc l'étendue réelle rendue plutôt qu'une approximation. La construction d'un objet jetable et la lecture de ses limites constituent la mesure la plus fiable que la bibliothèque puisse fournir
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Coordonnées et unités du résultat
La boîte englobante revient sous la forme de quatre bords, gauche, bas, droit et haut, et les deux dimensions en découlent par soustraction. La largeur est la droite moins la gauche et la hauteur est le haut moins le bas. Les deux sont exprimées en unités utilisateur PDF, où une unité représente un soixante-douzième de pouce, le même espace de coordonnées dans lequel vous positionnez du texte sur la page. Il n'y a pas d'unité d'appareil cachée ni de pixel impliqué à ce stade. Une largeur de 36 signifie un demi-pouce de page, quelle que soit la résolution de rendu finale
L'axe vertical fonctionne comme PDF le définit, avec Y augmentant vers le haut, c'est pourquoi la hauteur est le haut moins le bas plutôt que l'inverse. Ce détail a de l'importance lorsque vous faites avancer un curseur vers le bas d'une colonne. Vous mesurez la hauteur d'une ligne, puis vous la soustrayez de la ligne de base (baseline) actuelle pour trouver la suivante, car descendre dans la page signifie se déplacer vers des Y plus petits. Si votre destination est un écran plutôt que du papier, vous convertissez les unités utilisateur en pixels d'appareil avec la résolution d'affichage : une valeur en unités utilisateur multipliée par le PPP (DPI) et divisée par 72 donne des pixels, de sorte qu'une largeur de colonne que vous définissez en points peut être comparée à une suite mesurée avant que vous ne décidiez où va la coupure
Que se passe-t-il sur une entrée dégénérée
Les fonctions sont écrites pour échouer silencieusement. S'il n'y a pas de document ouvert, ou si l'objet texte ne peut pas être créé, le résultat est une étendue nulle plutôt qu'une exception levée. La largeur et la hauteur sont initialisées à zéro en haut et ne sont écrasées qu'une fois qu'une boîte englobante a été lue avec succès. Une chaîne vide, un document manquant, une police que la bibliothèque ne peut pas résoudre en un objet, chacun de ces cas renvoie zéro au lieu de lever une exception
Ce choix permet de garder une boucle de mesure simple, car une boucle qui parcourt des milliers de mots n'est pas le bon endroit pour gérer les exceptions à chaque itération. Le coût est que l'appelant porte la vérification. Une largeur nulle est une sentinelle, pas un fait concernant le texte, donc le code qui divise par une largeur mesurée ou suppose une valeur positive doit se prémunir contre zéro avant de lui faire confiance. Traitez zéro comme "n'a pas pu mesurer" et le contrat est clair ; ignorez-le et une entrée dégénérée devient silencieusement une disposition avec une colonne de glyphes se chevauchant
Un retour à la ligne glouton construit sur la mesure
Avec une fonction de largeur en main, le retour à la ligne (word wrap) est une courte boucle gloutonne. Vous divisez le paragraphe en mots, gardez une ligne actuelle et, pour chaque mot, vous mesurez ce que serait la ligne si vous y ajoutiez ce mot. Tant que la ligne d'essai correspond toujours à la largeur de la colonne, vous continuez à ajouter ; lorsqu'elle déborderait, vous videz la ligne actuelle avec AddText et en commencez une nouvelle avec le mot qui ne rentrait pas. L'accumulation se fait entièrement avec MeasureTextWidth, et la seule chose qui atteint jamais la page est une ligne dont vous avez déjà confirmé l'ajustement
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<WideString>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := WideString(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the final line
end;
La boucle mesure la ligne d'essai plutôt que de mesurer chaque mot et de faire la somme, car la largeur d'une ligne n'est pas la somme des largeurs de ses mots. Les espaces entre les mots contribuent, et une suite mesurée le capture directement. La règle gloutonne, ajuster autant de mots que la colonne le permet et couper au dernier qui s'adapte, est la même règle qui comble le fossé entre un AddText brut et un vrai paragraphe. L'appel de dessin n'a jamais été la partie difficile. C'est la mesure qui doit le précéder qui l'est, et c'est exactement ce que fournit l'assistant
Où cela s'intègre
La mesure est la couche entre la génération de contenu et son rendu, elle s'associe donc naturellement au reste d'un flux de travail de document créé à partir de zéro. Si vous assemblez des pages et placez du texte en premier lieu, le travail de base se trouve dans la création de documents PDF à partir de zéro avec le composant PDFium dans Delphi, où AddText et la configuration de la page sont entièrement couverts. Lorsque la police que vous mesurez importe autant que la chaîne, car les métriques dépendent de la face, l'analyse des propriétés de la police PDF avec le composant PDFium dans Delphi montre comment la bibliothèque signale les informations de police qui pilotent ces boîtes englobantes. Tous deux s'appuient sur la même liaison, le composant PDFium pour Delphi et Lazarus, où l'assistant de mesure est livré aux côtés des API de document, de page et de texte décrites sur ce blog