PDFium Component ajoute une couche de texte consultable aux pages PDF numérisées depuis Delphi via ApplyOcrSearchLayer. Il restitue chaque page sélectionnée, transmet les pixels à un fournisseur OCR que vous fournissez, et réécrit les mots reconnus sous forme d'objets texte invisibles positionnés au-dessus des mots dans la numérisation. L'image de page d'origine n'est jamais décodée, réencodée ou remplacée, de sorte que le résultat visuel est identique octet par octet à la page de départ
Le moteur de reconnaissance ne fait délibérément pas partie de la bibliothèque. PDFium expose le rendu de page, la projection de coordonnées, le chargement de polices, la création d'objets texte et les modes de rendu invisible, mais il ne contient aucun moteur OCR, et prétendre le contraire signifierait intégrer le produit de reconnaissance de quelqu'un d'autre dans un composant PDF. La reconnaissance réside plutôt derrière l'interface IPdfOcrProvider : la bibliothèque transmet des pixels BGRA à disposition fixe et origine en haut, et le fournisseur renvoie du texte Unicode, des valeurs de confiance et des quadrilatères de mots
Qu'est-ce qu'une couche de texte consultable, exactement ?
Un PDF numérisé est une image d'un document. Le contenu de la page est une seule grande image, et il n'y a rien à sélectionner, rechercher, copier ou indexer. Une couche de texte consultable ajoute de véritables objets texte par-dessus cette image avec le mode de rendu défini sur invisible, de sorte que les visionneuses n'affichent rien, mais que la sélection, la recherche et l'extraction trouvent les mots exactement là où ils apparaissent
Le positionnement est tout l'enjeu. Si le texte invisible est décalé de quelques points, les surlignages de sélection tombent à côté des mots plutôt que dessus, et copier un paragraphe produit un texte dans le mauvais ordre. C'est pourquoi la géométrie doit provenir des mêmes transformations que PDFium utilise pour restituer la page, plutôt que d'une estimation proportionnelle
Implémenter le fournisseur
Le contrat du fournisseur tient en une seule méthode. Elle reçoit un enregistrement d'image de page portant les dimensions, le pas de ligne, le DPI, le format de pixel et les octets de pixels eux-mêmes, plus un jeton d'annulation, et renvoie des mots ou un message d'erreur :
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels contient des lignes BGRA à origine en haut de Image.Stride octets.
// Transmettez-les à votre moteur, puis remplissez une entrée par mot reconnu
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
Des quadrilatères plutôt que des rectangles, car une numérisation est rarement parfaitement alignée sur la page. Un mot sur une page légèrement pivotée occupe un parallélogramme, et TPdfOcrQuad porte quatre points d'angle afin que les mots inclinés et pivotés conservent une région de sélection précise. Les moteurs qui ne rapportent que des boîtes alignées sur les axes peuvent utiliser FromRectangle, qui construit le quadrilatère dégénéré
Pourquoi les positions des mots ne peuvent-elles pas être mises à l'échelle proportionnellement ?
Il est tentant de convertir une coordonnée de pixel en coordonnée de page en divisant par la largeur de rendu et en multipliant par la largeur de page. Cela ne fonctionne que pour des pages sans rotation, avec une CropBox identique à la MediaBox, et une origine à zéro, et de nombreux documents numérisés échouent au moins l'une de ces conditions
PDFium Component projette individuellement chacun des quatre coins du quadrilatère via FPDF_DeviceToPage, la même projection que le moteur de rendu a utilisée pour produire les pixels, de sorte que les entrées /Rotate et les zones de recadrage décalées sont gérées par construction. La matrice affine de l'objet texte est ensuite construite à partir de trois des points projetés, les coins bas-gauche, bas-droit et haut-gauche, ce qui suffit exactement à exprimer la position, l'échelle, la rotation et le cisaillement
L'objet texte lui-même est créé à une taille de police unitaire afin que ses limites de police réelles puissent être mesurées, et les limites mesurées de l'objet sont ensuite projetées sur le quadrilatère cible. Dimensionner par une taille en points devinée en espérant qu'elle corresponde au mot numérisé dériverait à chaque substitution de police ; mesurer d'abord rend l'ajustement indépendant de la police utilisée par la couche
L'exécuter sur un document
L'enregistrement d'options contrôle la résolution, le filtrage et chaque budget. Le filtrage par confiance compte plus qu'il n'y paraît : les mots aberrants à faible confiance polluent durablement les résultats de recherche, et contrairement à un rendu erroné, personne ne s'en aperçoit avant qu'une recherche ne renvoie du non-sens :
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // résolution de reconnaissance
Options.MinConfidence := 0.60; // rejeter les mots incertains
Options.SkipPagesWithText := True; // laisser intactes les pages nativement numériques
Options.ContinueOnError := True; // une page défectueuse ne doit pas arrêter la tâche
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText mérite une attention particulière dans les archives mixtes. Un PDF qui porte déjà du texte réel, qu'il soit nativement numérique ou déjà traité, reçoit une seconde couche de texte si vous lui appliquez l'OCR aveuglément, et le doublon fait que l'extraction renvoie chaque mot deux fois. Le statut par page popsSkippedExistingText vous indique exactement quelles pages ont été laissées intactes
Budgets, annulation et confinement des échecs
Chaque quantité qu'un document malveillant ou simplement énorme peut gonfler dispose d'un plafond : pixels par page et au total, mots par page et au total, et caractères par mot. Tous sont vérifiés avant que la page ne soit écrite, pas après, et l'estimation de pixels est calculée à partir des dimensions de la page et du DPI avant qu'un quelconque bitmap ne soit alloué. Augmenter le DPI de 150 à 300 quadruple la mémoire par page, donc le plafond par page est le paramètre à ajuster en premier lorsqu'une tâche par lots commence à échouer sur des grands formats
Le jeton d'annulation traverse tout le chemin : rendu progressif, appel au fournisseur et boucle d'insertion mot par mot. Cela signifie qu'un utilisateur qui annule pendant la reconnaissance d'un fichier de 400 pages s'arrête en une seule page plutôt qu'à la fin du document, et le même schéma de jeton utilisé ailleurs dans le composant, décrit dans le rendu progressif annulable, s'applique ici sans changement
Le confinement des échecs se fait par page. La bibliothèque collecte les descripteurs d'objets qu'elle a insérés sur une page et appelle FPDFPage_GenerateContent une seule fois, après que tous les mots ont été placés. Si quelque chose échoue en cours de route, qu'il s'agisse d'une erreur de fournisseur ou d'un problème de police, les objets insérés sur cette page sont retirés en ordre inverse et le contenu de la page est régénéré, de sorte qu'une page en échec revient à son état d'origine au lieu de conserver une couche de texte à moitié faite. La boucle sur le document continue ou s'arrête ensuite selon ContinueOnError, et la page active est toujours restaurée
Vérifier que l'image n'a vraiment pas été touchée
La vérification la plus solide disponible est aussi la plus simple : restituez la page avant et après application de la couche à la même taille et comparez les bitmaps. Ils doivent être identiques octet par octet, car le texte invisible ne dessine rien et le flux d'image n'a jamais été décodé. Toute différence signifie qu'autre chose que la couche de texte a modifié la page
Après cela, vérifiez le côté texte en extrayant depuis le fichier traité et en confirmant que les positions des mots tombent sur la numérisation. Le chemin d'extraction est le même que celui décrit dans l'extraction de texte à partir de documents PDF, et pour une vérification visuelle rapide de l'alignement, restituer les pages en images comme dans la conversion de pages PDF en JPEG vous permet de superposer les boîtes de mots sur la numérisation
La superposition OCR, le rendu, l'extraction et l'édition s'exécutent tous sur le même objet document dans Delphi, C++Builder et Lazarus ; l'ensemble complet de l'API est décrit sur la page PDFium Component pour Delphi