Το HotPDF μετατρέπει σκαναρισμένες σελίδες PDF σε searchable PDF με Tesseract μέσω του HPDFCreateTesseractOCREngine, ενός factory που τυλίγει τοπικά εγκατεστημένο εκτελέσιμο Tesseract ως IHPDFOCREngine. Περνάτε εκείνη τη μηχανή στο ApplyLoadedOCRTextLayer, που αποδίδει κάθε σελίδα, τρέχει το Tesseract μία φορά ανά σελίδα, αναλύει το word-level TSV αποτέλεσμα του, και δεσμεύει αόρατη στρώση κειμένου Unicode για όλες τις ζητούμενες σελίδες σε μία συναλλαγή, ή για καμία
Ο λόγος που υπάρχει αυτός ο adapter είναι το πεδίο εφαρμογής. Η ενσωματωμένη OCR μηχανή template matching είναι σκόπιμα στενή: τυπωμένα από μηχανή γράμματα και ψηφία ASCII, τίποτα άλλο. Τιμολόγια με τονισμένα ονόματα, κινεζικά συμβόλαια, και αρχεία πολλών γλωσσών θέλουν πραγματικό recognizer με εκπαιδευμένα γλωσσικά μοντέλα, και το Tesseract είναι ο προφανής υποψήφιος επειδή είναι πρόγραμμα command-line που μπορείτε να προνοίατε δίπλα στην εφαρμογή σας. Το να καλείτε εξωτερικό πρόγραμμα από βιβλιοθήκη εγγράφων ακούγεται τετριμμένο. Δεν είναι, και ο περισσότερος ενδιαφέρων κώδικας στον adapter αφορά τι συμβαίνει όταν το πρόγραμμα παρασέρνει, κολλάει, ματαιώνεται, ή κληρονομεί πράγματα που δεν θα έπρεπε ποτέ να δει
Πώς οδηγεί το HotPDF το Tesseract από εφαρμογή Delphi;
Το HotPDF τρέχει το Tesseract ως κρυφή child διεργασία ανά σελίδα, ταϊζοντάς το αποδομένο bitmap και διαβάζοντας πίσω αρχείο TSV, και εκθέτει το αποτέλεσμα μέσω της ίδιας ραφής IHPDFOCREngine που χρησιμοποιεί η ενσωματωμένη μηχανή. Τίποτα κατάντη δεν αλλάζει: η αντιστοίχιση συντεταγμένων, ο χειρισμός περιστροφής, η επικύρωση Unicode, το φιλτράρισμα εμπιστοσύνης, και η ατομική δέσμευση είναι το pipeline στρώσης κειμένου που ήδη έχετε. Το factory ζει στη μονάδα HPDFTesseractRecognition και επικυρώνει πρόθυμα: το εκτελέσιμο πρέπει να υπάρχει, ο κατάλογος tessdata πρέπει να υπάρχει, το timeout πρέπει να είναι ανάμεσα σε 1 και 3.600.000 milliseconds, και ο αναγνωριστικός γλώσσας μπορεί να περιέχει μόνο γράμματα ASCII, ψηφία, _, και +. Εκείνος ο τελευταίος έλεγχος έχει σημασία επειδή το string γλώσσας καταλήγει σε γραμμή εντολών, και το eng+chi_sim είναι νόμιμη τιμή Tesseract ενώ οτιδήποτε με εισαγωγικά ή κενά δεν είναι
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// πετάει EArgumentException για λείπον εκτελέσιμο, λείπον tessdata,
// κακό αναγνωριστικό γλώσσας, ή timeout εκτός 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // αρκετά μοντέλα ενωμένα με '+'
120000); // όριο ανά σελίδα, default είναι 60000
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Options := THPDFOCRTextLayerOptions.Default; // 300 DPI, MinimumConfidence 0.5
Options.CancellationToken := Token;
// κενή λίστα σελίδων σημαίνει κάθε σελίδα· σελίδες με κείμενο παραλείπονται προεπιλεγμένα
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
Για κάθε σελίδα, το Recognize δημιουργεί ιδιωτικό κατάλογο κάτω από το temp μονοπάτι με όνομα HotPDF-OCR-{GUID}, αποθηκεύει το αποδομένο bitmap ως input.bmp, και εκκινεί tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, με κάθε όρισμα μονοπατιού σε εισαγωγικά κατά τους κανόνες escaping γραμμής εντολών Windows για ανάστροφες καθέτους και ενσωματωμένα εισαγωγικά. Η τιμή --dpi είναι η DPI απόδοσης από THPDFOCRTextLayerOptions.DPI, οπότε το Tesseract δεν χρειάζεται ποτέ να μαντέψει την ανάλυση από metadata εικόνας, και το --psm 3 ζητά πλήρως αυτόματο τμηματισμό σελίδας. Η μηχανή αναφέρει τον εαυτό της ως Tesseract (local CLI), που είναι ό,τι καταλήγει στο Info.EngineName. Το Tesseract και τα γλωσσικά του μοντέλα δεν παραδίδονται με το HotPDF· η εγκατάστασή τους είναι δουλειά της εφαρμογής
Γιατί είναι τόσο αυστηρός ο parser TSV;
Ο parser TSV στο HotPDF αποτυγχάνει ολόκληρη τη σελίδα σε κάθε παραμορφωμένη γραμμή, επειδή μια μερικώς αναλυμένη λίστα λέξεων παράγει στρώση κειμένου που διαφωνεί σιωπηλά με την εικόνα. Το TSV αποτέλεσμα του Tesseract έχει σταθερή κεφαλίδα δώδεκα στηλών, από το level έως το text, και το HotPDF συγκρίνει την πρώτη γραμμή απέναντι σε εκείνη την ακριβή κεφαλίδα μετά την αφαίρεση προαιρετικού byte order mark. Κάθε επόμενη γραμμή πρέπει να κοπεί σε ακριβώς δώδεκα πεδία, και το κόψιμο σταματά μετά την ενδέκατη στηλοθέτη ώστε στηλοθέτης μέσα στο αναγνωρισμένο κείμενο να μένει μέρος της λέξης αντί να δημιουργήσει δέκατη τρίτη στήλη. Μόνο γραμμές level 5 είναι λέξεις· τα levels 1 έως 4 περιγράφουν σελίδες, blocks, παραγράφους, και γραμμές, και παραλείπονται. Γραμμές level 5 των οποίων το κείμενο είναι κενό ή καθαρό whitespace παραλείπονται κι αυτές, επειδή κενή λέξη έχει box αλλά τίποτα να εντοπιστεί ή να αναζητηθεί. Όλα τα άλλα ελέγχονται σκληρά: ακέραια γεωμετρία, εμπιστοσύνη αναλυμένη με αναλλοίωτη μορφή en-US ώστε γερμανικό locale να μην διαβάζει το 93.5 ως σκουπίδια, box που βρίσκεται πλήρως μέσα στο bitmap, και εμπιστοσύνη ανάμεσα σε 0 και 100. Μία μόνο αποτυχία πετάει, η μηχανή επιστρέφει False, και ο πίνακας λέξεων καθαρίζεται. Τα regression tests περιλαμβάνουν ακριβώς εκείνη την περίπτωση: μία έγκυρη λέξη ακολουθούμενη από χαλασμένη γραμμή πρέπει να δώσει μηδέν λέξεις, όχι μία
// συμπυκνωμένο από τη λούπα level-5 στο HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // γραμμές σελίδας/block/παραγράφου/γραμμής
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // λέξεις whitespace δεν έχουν θέση
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // το pipeline περιμένει 0..1
Εκείνη η τελευταία γραμμή αλληλεπιδρά με default που ίσως δεν περιμένετε. Η εμπιστοσύνη Tesseract τρέχει από 0 έως 100, το pipeline δουλεύει σε 0 έως 1, και το THPDFOCRTextLayerOptions.MinimumConfidence defaults σε 0.5, οπότε κάθε λέξη Tesseract κάτω από 50 μετριέται στο Info.DroppedWordCount και δεν φτάνει ποτέ τη σελίδα. Σε καθαρό σκαν 300 DPI εκείνο είναι λογικό δάπεδο. Σε θορυβώδες fax μπορεί να πετάξει εκπληκτικό μερίδιο της σελίδας, και η σωστή κίνηση είναι να κοιτάξετε το πλήθος πεταμένων πριν χαμηλώσετε το όριο, επειδή λέξεις χαμηλής εμπιστοσύνης είναι ακριβώς εκείνες που έχουν τις περισσότερες πιθανότητες να είναι λάθος
Τι κληρονομεί η child διεργασία του Tesseract;
Η child διεργασία Tesseract κληρονομεί ακριβώς δύο handles από το HotPDF: ένα handle NUL για standard είσοδο και έξοδο, και handle αρχείου για standard error. Εκείνη η ακρίβεια είναι το ζητούμενο. Το CreateProcess με bInheritHandles = True είναι ο τρόπος που περνάτε standard handles σε child, αλλά από μόνο του περνά κάθε κληρονομήσιμο handle στη διεργασία host, συμπεριλαμβανομένων αρχείων, pipes, και events ανοιγμένων από άσχετο κώδικα στην εφαρμογή σας. Το child μετά κρατά εκείνα τα objects ζωντανά μέχρι να εξέλθει, οπότε αρχείο μένει κλειδωμένο ή pipe δεν βλέπει ποτέ το τέλος του όσο το Tesseract αλέθει μια σελίδα. Το HotPDF κλείνει εκείνο το χάσμα με εκτεταμένη εγγραφή εκκίνησης: STARTUPINFOEX, λίστα attributes που κουβαλά PROC_THREAD_ATTRIBUTE_HANDLE_LIST, και τη σημαία δημιουργίας EXTENDED_STARTUPINFO_PRESENT. Με τη λίστα handles στη θέση της, το bInheritHandles εξακολουθεί να πρέπει να είναι True, αλλά μόνο τα καταχωρισμένα handles διασχίζουν το όριο. Η ίδια σκέψης περιορισμού κινεί το απομόνωση PDF image codecs σε worker διεργασίες, όπου το child είναι αναξιόπιστος κώδικας· εδώ το child είναι αξιόπιστο, αλλά ο host δεν είναι ο μόνος ιδιοκτήτης του δικού του πίνακα handles
// σταθερές δειχνόμενες κατά όνομα· η πηγή περνά τις αριθμητικές τους τιμές
// και τα δύο handles δημιουργούνται με bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin και stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt στον ιδιωτικό κατάλογο
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // απαιτείται από τη λίστα handles
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Γιατί μπορεί ένα ματαιωμένο OCR run να μοιάζει αποτυχία μηχανής;
Ένα ματαιωμένο OCR run μοιάζει αποτυχία μηχανής επειδή το IHPDFOCREngine.Recognize επιστρέφει ένα μοναδικό Boolean, και το False σημαίνει και «απέτυχε το Tesseract» και «ο χρήστης πάτησε Ακύρωση». Ο adapter polled το token ακύρωσης και το timeout κάθε 25 milliseconds όσο τρέχει το child, και όταν το token πυροδοτείται πετάει μέσα στο Recognize, πιάνει τη δική του εξαίρεση, καθαρίζει, και επιστρέφει False με διαγνωστική. Αν το pipeline το μεταχειρίζονταν ως engine error, ο καλών θα έβλεπε otlsEngineError για δουλειά που ο χρήστης σταμάτησε επίτηδες. Το ApplyLoadedOCRTextLayer ελέγχει λοιπόν πρώτα το token όποτε το Recognize επιστρέφει False, και μετατρέπει το αποτέλεσμα σε αποτυχία μηχανής μόνο αν το token δεν είχε οριστεί. Εκείνη η σειρά διατηρεί το συμβόλαιο πολλών σελίδων: αναγνώριση, επικύρωση, λογαριασμός budget, και χτίσιμο περιεχομένου τρέχουν για κάθε ζητούμενη σελίδα πριν ανοίξει η συναλλαγή γράφου, οπότε ματαίωση στη σελίδα 40 από 50 αναφέρει otlsCancelled και αφήνει το έγγραφο, συμπεριλαμβανομένων των πρώτων 39 σελίδων, άθικτο. Δεν υπάρχει μερικώς searchable αρχείο προς εξήγηση αργότερα, και ο υπόλοιπος χειρισμός αποτυχιών ακολουθεί το ίδιο οριοθετημένο στυλ:
- Το timeout είναι ανά κλήση
Recognize, μετρημένο από την εκκίνησή της, οπότε το default 60.000 ms ισχύει για κάθε σελίδα και όχι για ολόκληρο το έγγραφο - Child που εξακολουθεί να τρέχει σε timeout ή ματαίωση τερματίζεται, περιμένεται έως 5 δευτερόλεπτα, και ο ιδιωτικός του κατάλογος σβήνεται σε block
finally - Το
output.tsvπερικόπτεται στα 64 MiB και τοstderr.txtστα 1 MiB, με έλεγχο όσο τρέχει το child όσο και αφού εξέλθει - Το πλήθος λέξεων και οι μονάδες κώδικα UTF-16 περικόπτονται ανά σελίδα από τα υπόλοιπα budgets
MaxWordsPerPage,MaxTotalWords, καιMaxTextCodeUnits, και η υπέρβαση τους αποτυγχάνει το run αντί να κόψει τη λίστα λέξεων - Η standard έξοδος πάει στο
NULεπειδή το Tesseract γράφειoutput.tsv, ενώ το standard error πάει σε αρχείο ώστε μη μηδενικός exit code αναφέρεται με έως 4.096 χαρακτήρες του δικού του παραπόνου της μηχανής, συνήθως ο ταχύτερος τρόπος να μάθετε ότι λείπει αρχείο.traineddata
Πώς οι αναγνωρισμένες λέξεις γίνονται αόρατη στρώση κειμένου
Το HotPDF γράφει λέξεις Tesseract ως αόρατο κείμενο χρησιμοποιώντας text rendering mode 3, τη κατάσταση ούτε-γέμισμα-ούτε-σχεδίαση του ISO 32000-1 §9.3.6, οπότε η σελίδα εξακολουθεί να δείχνει τη σκαναρισμένη εικόνα ενώ αναζήτηση και αντιγραφή δουλεύουν πάνω στις αναγνωρισμένες λέξεις. Το content stream ανοίγει BT με 3 Tr, και κάθε λέξη παίρνει μήτρα Tm στη baseline της, μέγεθος γραμματοσειράς παραγόμενο από το ύψος box σε pixels στη DPI απόδοσης, και οριζόντια κλίμακα Tz που τεντώνει το glyph run στο μετρημένο πλάτος box, που είναι ο λόγος που ένα highlight αναζήτησης προσγειώνεται πάνω στη λέξη στην εικόνα αντί να παρασέρνεται πάνω της
Το TSV του Tesseract έχει boxes αλλά όχι baselines, οπότε ο adapter αναφέρει κάθε λέξη χωρίς μία και το pipeline εκτιμά τη baseline στο ένα πέμπτο του ύψους box πάνω από το κάτω άκρο. Το ίδιο το κείμενο περνά από κοινόχρηστη unembedded γραμματοσειρά Type0 με κωδικοποίηση Identity-H και παραγόμενη CMap ToUnicode, ένα CID ανά διακριτό Unicode scalar σε όλο το run, που είναι ο τρόπος που κινεζικά, τονισμένα λατινικά, και χαρακτήρες supplementary-plane επιζούν όλα στην αντιγραφή και την αναζήτηση. Εκείνος ο σχεδιασμός έχει δύο όρια που αξίζει να δηλωθούν εκ των προτέρων: ένα run μπορεί να κουβαλά το πολύ 65.535 διακριτά scalars, και η unembedded γραμματοσειρά δεν ικανοποιεί την απαίτηση ενσωμάτωσης γραμματοσειρών του ISO 19005, οπότε έξοδος PDF/A θέλει ξεχωριστά ενσωματωμένη conforming γραμματοσειρά. Ο έλεγχος του αποτελέσματος είναι απλός και αξίζει αυτοματισμό: αποθηκεύστε, ξαναφορτώστε, και τρέξτε το συνηθισμένο μονοπάτι κειμένου φορτωμένου εγγράφου από το εξαγωγή κειμένου από φορτωμένο PDF στο Delphi· αν οι λέξεις γυρίσουν στις αναμενόμενες σελίδες, η στρώση είναι πραγματική
RapidOCR και άλλες μηχανές πάνω στο ίδιο πρωτόκολλο TSV
Το HotPDF ξαναχρησιμοποιεί τον ίδιο runner διεργασίας και parser TSV για το RapidOCR μέσω HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), που είναι η πιο χρήσιμη επιλογή για σκαν Απλοποιημένων Κινεζικών. Η γραμμή εντολών είναι πανομοιότυπη εκτός από το ότι το μονοπάτι του bridge script εισάγεται μετά το εκτελέσιμο Python, και η γλώσσα είναι καρφωμένη στο chi_sim. Το HotPDF παραδίδει το bridge ως tools/OCR/rapidocr_tsv.py· περιμένει τα πακέτα rapidocr και onnxruntime συν τρία τοπικά μοντέλα ONNX, απενεργοποιεί αυτόματα downloads μοντέλων, και γράφει TSV σχήματος Tesseract ώστε η πλευρά Delphi να μην χρειάζεται δεύτερο parser. Το όνομα μηχανής που αναφέρεται στο Info.EngineName είναι RapidOCR (local ONNX). Εκείνο το σχήμα υπονοεί τη γενική συνταγή: Οποιονδήποτε recognizer μπορείτε να τυλίξετε σε μικρό script που δέχεται τη λίστα ορισμάτων στυλ Tesseract και βγάζει το TSV δώδεκα στηλών κληρονομεί δωρεάν την απομόνωση handles, το timeout, την ακύρωση, τα budgets εξόδου, και τη δέσμευση όλα-ή-τίποτα. Οι adapters είναι μόνο για Windows, τρέχουν μία σελίδα τη φορά συγχρόνως, και δεν ισιώνουν ούτε προεπεξεργάζονται την εικόνα πέρα από ό,τι παράγει ο renderer, οπότε η ποιότητα εικόνας που μπαίνει εξακολουθεί να ορίζει το ταβάνι αυτού που βγαίνει
Οι adapters Tesseract και RapidOCR, ο writer αόρατης στρώσης κειμένου, ο renderer σελίδων που τους ταίζει, και η εξαγωγή κειμένου που επαληθεύει το αποτέλεσμα παραδίδονται όλα στο ίδιο native VCL component για Delphi και C++Builder. Αν προσθέτετε OCR σε εφαρμογή καταγραφής ή αρχειοθέτησης εγγράφων, το HotPDF Delphi PDF component σας δίνει το pipeline με μόνο την ίδια την OCR μηχανή να απομένει προς εγκατάσταση