PDF Library for Delphi peut faire correspondre du texte par équivalence canonique plutôt que par unité de code, de sorte qu'une requête saisie sous forme de caractère précomposé trouve un contenu stocké sous forme de lettre de base plus une marque combinante, et inversement. Deux options de recherche le contrôlent : soCanonicalEquivalent active la normalisation Unicode pendant la correspondance, et soGraphemeClusters contraint chaque résultat et chaque étape de caractère générique à des clusters de graphèmes entiers
Le bug que cela corrige est l'un des plus signalés et des moins compris dans la recherche de documents. Un utilisateur recherche un nom, ne voit aucun résultat, copie le nom hors du document, le colle dans le champ de recherche, et le trouve. Rien n'est cassé de manière évidente : les deux chaînes paraissent identiques, s'impriment de manière identique, et se comparent comme différentes, car l'une est U+00E9 et l'autre est U+0065 suivi de U+0301
Pourquoi le même mot se compare-t-il comme différent ?
Unicode autorise plusieurs encodages pour le même caractère abstrait. Les lettres latines avec diacritiques existent sous forme de points de code précomposés et sous forme de séquences base plus combinante. Les syllabes hangul existent sous forme de syllabes précomposées et sous forme de jamo décomposés. Lequel un PDF contient dépend du producteur, de la plateforme, et parfois de la police, et rien de tout cela n'est visible pour la personne qui effectue la recherche
La raison pour laquelle un simple repli de casse ne résout pas ce problème est structurelle et non accidentelle. Le repli de casse et le repli d'accent sont bijectifs au niveau de l'unité de code : la chaîne repliée a la même longueur que l'originale, donc une position de correspondance dans le texte replié est une position de correspondance dans l'original. La normalisation n'est pas bijective. Un caractère précomposé devient deux ou trois unités de code, une séquence décomposée se réduit à une seule, et après cette transformation, les positions ne correspondent plus au texte que vous avez extrait
Garder les coordonnées de résultat pointées vers le texte original
C'est la partie qui détermine si la recherche normalisée est utilisable plutôt que simplement correcte. Chaque unité de code produite par la normalisation enregistre la position de début et de fin du texte UTF-16 original qui l'a produite. Les décompositions récursives héritent de la plage source de leur parent, les compositions fusionnent les plages de leurs entrées, et lorsqu'une correspondance est trouvée, la bibliothèque parcourt l'intervalle de projection à la recherche du plus petit début et de la plus grande fin
L'effet est que MatchStart, MatchLength, les chaînes de contexte et les deux points d'entrée de remplacement continuent tous d'adresser le texte extrait original, pas l'intermédiaire normalisé. Sans cette projection, une recherche normalisée pourrait vous dire qu'un résultat existe mais pas de manière fiable où il se trouvait, ce qui rend le surlignage erroné et le caviardage dangereux
Le normaliseur lui-même est autonome : des tables compactes pour la décomposition canonique, la composition et la classe combinante canonique tirées d'Unicode 15.1, le hangul étant traité par les règles algorithmiques plutôt que par des entrées de table. Rien n'est chargé depuis un fichier de données externe et aucune API de normalisation de plateforme n'est appelée, de sorte qu'un service Windows, un démon Linux et un build FPC produisent tous des résultats identiques sur la même entrée
Rechercher avec équivalence canonique
Les options forment un ensemble, donc l'équivalence canonique se combine avec les comportements existants tels que la correspondance de mot entier, les caractères génériques et le repli insensible aux diacritiques :
uses
PDFlibrary;
var
Lib: TPDFlib;
Hits: array of TPDFlibSearchHit;
Found, I: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('contracts.pdf', '');
SetLength(Hits, 500);
Found := Lib.SearchText('Bäcker', [soCanonicalEquivalent, soWholeWord],
'', Hits); // plage de page vide = document entier
for I := 0 to Found - 1 do
Log(Format('page %d: "%s" at %d (%d chars)',
[Hits[I].Page, Hits[I].MatchText, Hits[I].MatchStart,
Hits[I].MatchLength]));
finally
Lib.Free;
end;
end;
La normalisation est optionnelle pour une bonne raison. Construire le texte NFD et sa projection de positions coûte du travail, et la plupart des recherches sur des documents purement ASCII n'en ont jamais besoin. Lorsque l'option est utilisée, chaque bloc de texte met en cache deux formes transformées, l'une avec les marques combinantes retirées et l'autre sans, de sorte qu'un lot de requêtes sur le même bloc se normalise une fois plutôt qu'une fois par requête. Le repli de casse continue d'emprunter sans changement le chemin bijectif moins coûteux
Que casse-t-on sans limites de cluster de graphèmes ?
Les unités de code ne sont pas des caractères, et les caractères ne sont pas ce que les utilisateurs perçoivent. Un emoji de drapeau est deux points de code d'indicateur régional. Un emoji de famille est plusieurs points de code joints par des joncteurs de largeur nulle. Un conjoint indic est une consonne, un virama et une autre consonne. Une lettre avec deux accents superposés est trois points de code. Faire correspondre ou couper au milieu de l'un de ces éléments produit un fragment qui s'affiche comme un charabia
soGraphemeClusters contraint les deux extrémités de chaque résultat, littéral ou générique, à des limites de cluster de graphèmes étendues complètes. La segmentation implémente les règles étendues : appariement CR et LF, caractères de contrôle, classes de syllabes hangul, Extend et SpacingMark, Prepend, séquences ZWJ d'emoji, appariement d'indicateur régional et coupures de conjoints indic. Une limite n'est jamais produite à l'intérieur d'une paire de substituts, ce qui élimine à elle seule toute une classe de résultats corrompus sur tout contenu au-delà du plan multilingue de base
L'option régit aussi la consommation des caractères génériques, ce qui est l'endroit où une implémentation naïve couperait encore incorrectement. Le caractère générique à un caractère avance exactement d'un cluster complet, et le retour en arrière pour le caractère générique de plage ne se déplace qu'entre les limites de cluster :
// Sans soGraphemeClusters, « ? » peut consommer la moitié d'un cluster et
// renvoyer un résultat dont le texte se termine par une marque combinante pendante
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// Les mêmes limites protègent le remplacement, si bien que le caviardage et
// la réécriture de contenu ne scindent jamais un emoji ou une lettre accentuée
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
Choisir les options pour une charge de travail réelle
Trois combinaisons couvrent la plupart des cas. Pour un champ de recherche de documents interne, soCanonicalEquivalent plus soDiacriticInsensitive offre le comportement indulgent que les utilisateurs attendent, faisant correspondre les deux formes d'encodage ainsi que les orthographes accentuées et non accentuées. Pour une recherche juridique ou de conformité, où un faux positif a un coût, utilisez soCanonicalEquivalent avec soCaseSensitive et soWholeWord et laissez le repli d'accent désactivé, de sorte que l'équivalence soit exacte et indépendante de l'encodage
Pour tout ce qui modifie le document, ajoutez soGraphemeClusters sans exception. Une recherche qui renvoie une plage légèrement erronée n'induit en erreur qu'un lecteur ; un remplacement ou un caviardage qui utilise la même plage erronée inscrit l'erreur dans le fichier. Les conséquences d'une plage de suppression erronée sont traitées dans le véritable caviardage et la suppression de contenu
Lorsque le débit compte, préférez les points d'entrée par lot. SearchTextBatch exécute chaque requête non vide pendant que les blocs de texte de chaque page sont résidents, ce qui évite de réextraire une page par requête et réutilise la normalisation mise en cache, et les variantes en flux émettent les résultats sans tampon dimensionné par l'appelant. Le modèle d'extraction sous-jacent est décrit dans la recherche de texte et l'énumération des éléments de page
Écritures où ceci n'est pas optionnel
Pour le coréen, l'équivalence canonique fait la différence entre trouver un nom et ne pas le trouver, car les syllabes précomposées et les jamo décomposés sont tous deux courants dans les documents réels. Pour le vietnamien, les diacritiques superposés rendent la forme de composition entièrement dépendante du producteur. Pour les écritures indiques, le traitement des conjoints détermine si une limite de résultat tombe à un endroit lisible. Pour le japonais et le chinois, le côté recherche est relativement simple, bien que le côté mise en page ne le soit pas, comme décrit dans l'écriture verticale pour le japonais et le chinois
La règle empirique est brève : si le corpus contient une langue autre que l'anglais, activez l'équivalence canonique et mesurez le coût avant de décider qu'il est trop élevé. Dans la plupart des ensembles de documents, il ne l'est pas, et l'alternative est une fonctionnalité de recherche qui échoue silencieusement précisément sur les noms que vos utilisateurs tiennent le plus à trouver
La recherche, l'extraction, le caviardage et la réécriture de texte sensibles à Unicode partagent un même moteur pour Delphi, C++Builder et Free Pascal ; la liste complète des fonctionnalités se trouve sur la page PDF Library for Delphi