Une page de texte PDF expose des caractères et des boîtes, jamais des lignes. PDFium Component construit une ligne visuelle en regroupant les boîtes de caractères dont les centres verticaux tombent dans la moitié de la hauteur du caractère graine, en balayant vers l extérieur depuis le caractère cliqué jusqu à ce que la tolérance soit dépassée. Chaque chemin de sélection du visualiseur appelle cet unique helper, donc souris, clavier et code s accordent
Le symptôme qui vous amène à chercher ceci est spécifique et désagréable. Un utilisateur triple-clique un paragraphe dans un rapport à deux colonnes et obtient la moitié de la page. Ou il triple-clique une cellule de tableau et la sélection avale toute la ligne plus le numéro de page dans le pied de page. Le visualiseur n est pas cassé ; il pose une question à laquelle le fichier ne peut pas répondre. Il n y a pas de ligne dans un PDF à sélectionner, et toute implémentation qui prétend le contraire devine. Cet article porte sur le fait de rendre la supposition délibérée et de la rendre cohérente. Si ce dont vous avez réellement besoin est d extraire du texte d un document, voir l extraction de texte de documents PDF avec PDFium ; si vous mettez en page du texte et avez besoin de largeurs, voir la mesure de texte et le retour à la ligne. Ici le sujet est plus étroit : décider où une ligne visuelle commence et se termine, et sélectionner exactement cela
Pourquoi une page de texte PDF n a-t-elle pas d objets ligne ?
Parce qu un flux de contenu PDF décrit du dessin, pas de la structure. La norme ISO 32000-1 §9.4 définit un objet texte comme une paire BT / ET contenant des opérateurs de positionnement et d affichage. Les opérateurs de positionnement du §9.4.2 (Td, TD, Tm, T*) déplacent une matrice de texte à travers la page, et les opérateurs d affichage du §9.4.3 (Tj, TJ, ', ") peignent des glyphes là où cette matrice pointe actuellement. Rien dans ce modèle ne dit « cette série de glyphes est une ligne ». Une ligne est ce qu un humain voit après que le dessin est terminé
Les producteurs aggravent cela de façons que vous ne contrôlez pas. Un paragraphe justifié peut être émis comme un tableau TJ par ligne, ou comme un Tj par mot avec un Tm explicite avant chacun, ou comme une seule opération d affichage avec des ajustements de crénage portant l espacement. Une mise en page à deux colonnes peut émettre la colonne de gauche de haut en bas puis la colonne de droite, ou elle peut les entrelacer si le producteur a parcouru sa propre liste d objets interne dans un ordre différent. La séquence de caractères que PDFium vous remet suit le flux de contenu, et le flux de contenu suit ce que l application génératrice a bien voulu faire. Donc les deux fonctions que vous obtenez réellement sont FPDFText_CountChars, qui rapporte combien de caractères la page contient, et FPDFText_GetCharBox, qui renvoie la boîte englobante d un caractère dans l espace page. C est tout le vocabulaire brut. Tout ce qui se trouve au-dessus, mots, lignes, paragraphes, colonnes, est une inférence que vous effectuez sur la géométrie
Pourquoi la détection de CR et LF est-elle le mauvais test ?
Parce que les caractères contre lesquels vous testeriez ne sont pas fiablement présents, et quand ils le sont, ils ne sont pas fiablement les vôtres. PDFium injecte des caractères synthétiques dans la page de texte pour rendre le texte extrait lisible : un espace là où deux séries sont visuellement séparées, un CR ou LF là où la série suivante commence sur une nouvelle ligne de base. FPDFText_IsGenerated existe précisément pour que vous puissiez les distinguer des caractères issus du fichier, et PDFium Component l expose comme la propriété CharacterGenerated
Divisez sur ces caractères et vous héritez de chaque jugement que PDFium a fait en les synthétisant. Un saut de ligne dur à l intérieur d un paragraphe enveloppé et un retour à la ligne souple paraissent identiques après synthèse. Une ligne de tableau que le producteur a émise cellule par cellule peut ne recevoir aucun saut du tout entre la dernière cellule et la première cellule de la ligne suivante, car les lignes de base se trouvent être suffisamment proches. Pendant ce temps, un titre suivi de texte de corps à une taille différente peut recevoir deux sauts là où un humain n en voit qu un. Les caractères générés sont une commodité de rendu pour l extraction de page entière ; ce n est pas un modèle de ligne, et ils se dégradent précisément dans les documents où la sélection compte le plus
Regrouper les boîtes de caractères par centre vertical
Le signal fiable est la géométrie. Prenez le caractère que l utilisateur a cliqué comme graine, calculez le centre vertical de sa boîte, et parcourez vers l extérieur dans les deux directions tant que les boîtes voisines conservent leurs centres verticaux dans la tolérance. PDFium Component utilise la moitié de la hauteur de la boîte graine comme cette tolérance, avec un plancher de 0.5 unités de page afin que des boîtes dégénérées, un point, une espace fine, un glyphe avec une boîte de hauteur presque nulle, ne réduisent pas la tolérance à rien et ne coupent pas la ligne après un seul caractère
function TPdfView.LineRangeAt(TxtPage: FPDF_TEXTPAGE; CharIndex: Integer;
out StartIndex, Count: Integer): Boolean;
var
Lo, Hi, Total: Integer;
SeedBox, Box: TPdfRectangle;
SeedYMid, BoxYMid, HalfH: Double;
begin
Result := False;
StartIndex := -1;
Count := 0;
Total := FPDFText_CountChars(TxtPage);
if (CharIndex < 0) or (CharIndex >= Total) then
Exit;
if FPDFText_GetCharBox(TxtPage, CharIndex, SeedBox.Left, SeedBox.Right,
SeedBox.Bottom, SeedBox.Top) = 0 then
Exit;
SeedYMid := (SeedBox.Top + SeedBox.Bottom) / 2;
HalfH := Abs(SeedBox.Top - SeedBox.Bottom) / 2;
if HalfH < 0.5 then // floor for degenerate boxes
HalfH := 0.5;
Lo := CharIndex;
Hi := CharIndex;
while Lo > 0 do
begin
if FPDFText_GetCharBox(TxtPage, Lo - 1, Box.Left, Box.Right,
Box.Bottom, Box.Top) = 0 then
Break;
BoxYMid := (Box.Top + Box.Bottom) / 2;
if Abs(BoxYMid - SeedYMid) > HalfH then
Break;
Dec(Lo);
end;
while Hi < Total - 1 do
begin
if FPDFText_GetCharBox(TxtPage, Hi + 1, Box.Left, Box.Right,
Box.Bottom, Box.Top) = 0 then
Break;
BoxYMid := (Box.Top + Box.Bottom) / 2;
if Abs(BoxYMid - SeedYMid) > HalfH then
Break;
Inc(Hi);
end;
StartIndex := Lo;
Count := Hi - Lo + 1;
Result := True;
end;
Trois détails de cette boucle gagnent leur place. La tolérance dérive de la graine plutôt que d une constante, donc un titre de 24pt obtient une bande large et un texte de note de bas de page de 7pt en obtient une étroite, et aucun ne vole de caractères à l autre. La comparaison utilise les centres verticaux plutôt que les lignes de base ou les hauts de boîte, ce qui garde un exposant, une série de taille différente en ligne, ou une phrase à police mixte sur la même ligne que ses voisins. Et un FPDFText_GetCharBox échoué termine le balayage plutôt que d être ignoré, car un caractère sans géométrie récupérable ne donne aucune preuve dans un sens ou dans l autre, et continuer au-delà lui permettrait de sauter à travers une véritable limite sur la foi d un caractère plus loin
Pourquoi chaque chemin de sélection doit-il partager un seul helper ?
Parce que trois chemins de code qui implémentent chacun « la ligne » divergeront, et ils divergeront silencieusement. Dans PDFium Component, l extension par triple-clic, Shift+Home, Shift+End, et la méthode publique SelectLineAt résolvent tous leurs limites via le même appel LineRangeAt. Le triple-clic l amorce depuis l ancre de sélection ; les touches shift l amorcent depuis le curseur de sélection et ne déplacent que cette extrémité ; SelectLineAt l amorce depuis un index de caractère fourni par l appelant et remet le résultat à SelectTextRange, le même validateur de plage que le chemin souris utilise. Dupliquez plutôt la logique et l échec n est pas un plantage, c est une dérive lente. Quelqu un ajuste la tolérance de triple-clic pour corriger un rapport à interligne serré, et maintenant Shift+End s arrête un caractère avant où le triple-clic s arrête sur le même paragraphe. Un utilisateur sélectionne une ligne avec la souris, l étend avec le clavier, et regarde la sélection rétrécir. Parce que SelectLineAt alimente le pipeline de sélection ordinaire, la sélection programmatique reste aussi indépendante du fait que la saisie souris soit activée, et obtient quand même la validation de plage, le repeint, et la notification OnSelectionChange gratuitement
// Select the visual line under a client-space point, then read it back
procedure TForm1.SelectLineUnderCursor(X, Y: Integer);
var
CharIndex: Integer;
begin
CharIndex := PdfView1.CharacterIndexAtPos(X, Y, 6.0, 6.0);
if CharIndex < 0 then
Exit;
if PdfView1.SelectLineAt(PdfView1.CurrentPage, CharIndex) then
Memo1.Lines.Add(PdfView1.SelectedText);
end;
Notez les arguments de tolérance sur CharacterIndexAtPos. Le test de collision a son propre jeu, exprimé en unités de page, et c est une préoccupation séparée de la tolérance de ligne. Un clic qui atterrit dans l interlignage entre deux lignes se résout au caractère le plus proche dans cette boîte ; le balayage de ligne s exécute alors depuis quel que soit le caractère qui en a résulté. Alimenter une tolérance de collision trop généreuse dans la graine est l un des moyens les plus faciles de sélectionner une ligne que l utilisateur ne pointait pas
Deux espaces d index : index de caractère et index de texte
Une fois que vous avez une plage, résistez à l envie de l utiliser comme décalage de chaîne. FPDFText_GetText renvoie le texte de la page comme un tampon UTF-16, mais ses index ne sont pas le même espace d index que les index de caractère utilisés par FPDFText_GetCharBox et FPDFText_CountChars. Les caractères générés discutés plus haut se trouvent dans le tampon texte tout en occupant des emplacements de caractère sans géométrie utilisable, et les deux numérotations dérivent l une de l autre à travers la page. Les ponts sont FPDFText_GetTextIndexFromCharIndex et FPDFText_GetCharIndexFromTextIndex, enveloppés par PDFium Component comme CharacterIndexToTextIndex et TextIndexToCharacterIndex
var
TextStart, TextEnd: Integer;
begin
// char-index range from LineRangeAt -> offsets into the page text buffer
TextStart := Pdf.CharacterIndexToTextIndex(StartIndex);
TextEnd := Pdf.CharacterIndexToTextIndex(StartIndex + Count - 1);
if (TextStart >= 0) and (TextEnd >= TextStart) then
Caption := Pdf.Text(TextStart, TextEnd - TextStart + 1);
end;
La direction qui mord le plus fort est la direction inverse. Une recherche implémentée sur la chaîne extraite vous donne des index de texte, et passer ceux-ci directement à une API de boîte ou de sélection adresse silencieusement les mauvais caractères, avec une erreur qui grandit à mesure que vous descendez dans la page. Convertissez avec TextIndexToCharacterIndex avant que quoi que ce soit de géométrique ne touche le nombre. Les paires de substitution ajoutent un second problème de décalage indépendant par-dessus cela, couvert dans l article sur les emoji, le CJK, et les paires de substitution
Où l heuristique plie
Soyez honnête avec vous-même sur les limites, car elles sont réelles et atteignables. Le texte pivoté est le cas le plus clair : une boîte de caractère est un rectangle aligné sur les axes dans l espace page, donc pour du texte pivoté à 90 degrés, les boîtes d une ligne visuelle ont des centres verticaux dispersés à travers la page, et le balayage s arrête presque immédiatement. Ce que vous obtenez est une sélection courte plutôt qu une fausse, ce qui est le meilleur mode de défaillance, mais c est quand même un échec. Les modes d écriture verticale se comportent de la même façon pour la même raison. Les mises en page à deux colonnes fonctionnent quand les colonnes sont décalées verticalement l une de l autre et échouent quand elles ne le sont pas. Si les deux colonnes partagent une grille de ligne de base, les caractères de la colonne de droite se trouvent dans la tolérance de la ligne de la colonne de gauche, et le balayage traversera tout droit la gouttière, car en géométrie pure il n y a rien là pour l arrêter. Détecter cela nécessite un test d écart horizontal par-dessus le regroupement vertical, et choisir le seuil d écart est son propre jugement sur les documents pour lesquels vous êtes prêt à vous tromper. Les tailles de police mixtes sont le cas que la tolérance relative à la graine gère bien : une portion de code 8pt en ligne dans un corps de texte 11pt garde son centre dans la bande, et un titre 24pt sur la ligne de base suivante n attire pas la ligne de corps vers lui
La sémantique de sélection de ligne décrite ici est livrée dans PDFium Component pour Delphi et C++Builder, aux côtés du test de collision, de la plage de sélection, et des API d index de texte utilisées dans les exemples ; la page produit porte la référence complète pour le modèle de page de texte et de sélection