PDF Library for Delphi può confrontare il testo per equivalenza canonica anziché per unità di codice, così una query digitata come carattere precomposto trova contenuto memorizzato come lettera base più segno diacritico combinante, e viceversa. Due opzioni di ricerca lo controllano: soCanonicalEquivalent abilita la normalizzazione Unicode durante il confronto, e soGraphemeClusters vincola ogni corrispondenza e ogni passo di wildcard a interi grapheme cluster
Il bug che questo risolve è uno dei più segnalati e meno compresi nella ricerca nei documenti. Un utente cerca un nome, non vede risultati, copia il nome dal documento, lo incolla nella casella di ricerca, e lo trova. Nulla è rotto in modo evidente: le due stringhe appaiono identiche, si stampano identiche, e il confronto le dà diseguali, perché una è U+00E9 e l'altra è U+0065 seguito da U+0301
Perché la stessa parola risulta diseguale nel confronto?
Unicode permette diverse codifiche per lo stesso carattere astratto. Le lettere latine con segni diacritici esistono come code point precomposti e come sequenze di base più combinante. Le sillabe hangul esistono come sillabe precomposte e come jamo decomposti. Quale delle due un PDF contiene dipende dal produttore, dalla piattaforma, e a volte dal font, e nulla di tutto ciò è visibile a chi effettua la ricerca
Il motivo per cui il semplice case folding non risolve questo problema è strutturale, non incidentale. Il case folding e il folding degli accenti sono uno a uno a livello di unità di codice: la stringa foldata ha la stessa lunghezza dell'originale, quindi una posizione di corrispondenza nel testo foldato è una posizione di corrispondenza nell'originale. La normalizzazione non è uno a uno. Un carattere precomposto diventa due o tre unità di codice, una sequenza decomposta si riduce di nuovo a una, e dopo quella trasformazione, le posizioni non si allineano più con il testo estratto
Mantenere le coordinate delle corrispondenze puntate sul testo originale
Questa è la parte che determina se la ricerca normalizzata sia utilizzabile e non solo corretta. Ogni unità di codice prodotta dalla normalizzazione registra la posizione di inizio e fine del testo UTF-16 originale che l'ha prodotta. Le decomposizioni ricorsive ereditano l'intervallo sorgente del proprio genitore, le composizioni uniscono gli intervalli dei propri input, e quando viene trovata una corrispondenza la libreria scansiona l'intervallo di mappatura per l'inizio più piccolo e la fine più grande
L'effetto è che MatchStart, MatchLength, le stringhe di contesto ed entrambi i punti di ingresso di sostituzione continuano tutti a fare riferimento al testo estratto originale, non all'intermedio normalizzato. Senza quella mappatura, una ricerca normalizzata potrebbe dire che esiste una corrispondenza ma non in modo affidabile dove si trovava, il che rende sbagliata l'evidenziazione e pericolosa la redazione
Il normalizzatore stesso è autonomo: tabelle compatte per decomposizione canonica, composizione e classe di combinazione canonica da Unicode 15.1, con l'Hangul gestito dalle regole algoritmiche anziché da voci di tabella. Nulla viene caricato da un file di dati esterno e nessuna API di normalizzazione di piattaforma viene chiamata, così un servizio Windows, un demone Linux e una build FPC producono tutti risultati identici sullo stesso input
Cercare con equivalenza canonica
Le opzioni sono un insieme, quindi l'equivalenza canonica si combina con i comportamenti esistenti come la corrispondenza per parola intera, i wildcard e il folding insensibile ai segni diacritici:
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); // intervallo di pagine vuoto = intero documento
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 normalizzazione è opt-in per un motivo. Costruire il testo NFD e la sua mappatura di posizione costa lavoro, e la maggior parte delle ricerche su documenti solo ASCII non ne ha mai bisogno. Quando l'opzione viene usata, ogni blocco di testo memorizza in cache due forme trasformate, una con i segni combinanti rimossi e una senza, così un lotto di query sullo stesso blocco normalizza una volta anziché una volta per query. Il case folding continua a percorrere invariato il percorso uno a uno più economico
Cosa si rompe senza confini di grapheme cluster?
Le unità di codice non sono caratteri, e i caratteri non sono ciò che gli utenti percepiscono. Un'emoji bandiera è due code point regional indicator. Un'emoji famiglia è diversi code point uniti da zero-width joiner. Un conjunct indico è una consonante, un virama e un'altra consonante. Una lettera con due accenti sovrapposti è tre code point. Confrontare o tagliare nel mezzo di uno qualsiasi di questi produce un frammento che si renderizza come spazzatura
soGraphemeClusters vincola entrambe le estremità di ogni corrispondenza, letterale o wildcard, a confini completi di extended grapheme cluster. La segmentazione implementa le regole estese: accoppiamento CR e LF, caratteri di controllo, classi di sillabe Hangul, Extend e SpacingMark, Prepend, sequenze ZWJ per emoji, accoppiamento di regional indicator e interruzioni conjunct indiche. Un confine non viene mai prodotto dentro una coppia surrogata, il che da solo elimina un'intera classe di risultati corrotti su qualsiasi contenuto oltre il piano multilingue di base
L'opzione governa anche il consumo dei wildcard, che è dove un'implementazione ingenua taglierebbe comunque in modo scorretto. Il wildcard a carattere singolo avanza esattamente di un cluster completo, e il backtracking per il wildcard di sequenza si muove solo tra confini di cluster:
// Senza soGraphemeClusters, "?" può consumare metà di un cluster e
// restituire una corrispondenza il cui testo termina con un segno combinante penzolante
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// Gli stessi confini proteggono la sostituzione, così la redazione e
// la riscrittura del contenuto non dividono mai un'emoji o una lettera accentata
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
Scegliere le opzioni per un carico di lavoro reale
Tre combinazioni coprono la maggior parte dei casi. Per una casella di ricerca documenti interna, soCanonicalEquivalent più soDiacriticInsensitive offre il comportamento indulgente che gli utenti si aspettano, corrispondendo sia alle forme di codifica sia alle grafie accentate e non accentate. Per la ricerca legale o di conformità, dove un falso positivo ha un costo, usa soCanonicalEquivalent con soCaseSensitive e soWholeWord e lascia disattivato il folding degli accenti, così l'equivalenza è esatta e indipendente dalla codifica
Per qualsiasi cosa che modifichi il documento, aggiungi soGraphemeClusters senza eccezioni. Una ricerca che restituisce un intervallo leggermente sbagliato inganna solo un lettore; una sostituzione o redazione che usa lo stesso intervallo sbagliato scrive l'errore nel file. Le conseguenze di sbagliare gli intervalli di rimozione sono trattate in vera redazione e rimozione del contenuto
Quando conta il throughput, preferisci i punti di ingresso batch. SearchTextBatch esegue ogni query non vuota mentre i blocchi di testo di ogni pagina sono residenti, il che evita di riestrarre una pagina per ogni query e riutilizza la normalizzazione in cache, e le varianti in streaming emettono le corrispondenze senza un buffer dimensionato dal chiamante. Il modello di estrazione sottostante è descritto in ricerca testo ed enumerazione degli elementi di pagina
Script per cui questo non è opzionale
Per il coreano, l'equivalenza canonica è la differenza tra trovare un nome e non trovarlo, perché sillabe precomposte e jamo decomposti sono entrambi comuni nei documenti reali. Per il vietnamita, i segni diacritici sovrapposti rendono la forma di composizione interamente dipendente dal produttore. Per gli script indici, la gestione dei conjunct decide se un confine di corrispondenza cade in un punto leggibile. Per giapponese e cinese, il lato ricerca è comparativamente semplice, anche se il lato layout non lo è, come descritto in scrittura verticale per giapponese e cinese
La regola pratica è breve: se il corpus contiene una qualsiasi lingua diversa dall'inglese, attiva l'equivalenza canonica e misura il costo prima di decidere che sia troppo elevato. Nella maggior parte degli insiemi di documenti non lo è, e l'alternativa è una funzionalità di ricerca che fallisce silenziosamente esattamente sui nomi che agli utenti importa di più trovare
La ricerca, l'estrazione, la redazione e la riscrittura del testo consapevoli di Unicode condividono un unico motore per Delphi, C++Builder e Free Pascal; l'elenco completo delle funzionalità si trova nella pagina di PDF Library per Delphi