Το HotPDF Component εξαγάγει κείμενο Unicode από οποιοδήποτε PDF φορτώνετε στο Delphi μέσω δύο κλήσεων: Η ExtractLoadedPageText επιστρέφει το κείμενο ροής ανάγνωσης μιας σελίδας, και η ExtractLoadedPageTextLayout (προστέθηκε στην έκδοση v2.263.0) ανακατασκευάζει την οπτική διάταξη της σελίδας ως απλό κείμενο, έτσι ώστε οι στήλες, η εσοχή και η ευθυγράμμιση πινάκων να επιβιώνουν στο αποτέλεσμα. Και οι δύο λειτουργούν σε έγγραφα που δεν δημιουργήθηκαν με το HotPDF, η οποία είναι η περίπτωση που πραγματικά έχει σημασία: το τιμολόγιο που σας έστειλε ένας πελάτης μέσω email, η αναφορά που παρέδωσε ένα γραφείο σάρωσης, το συμβόλαιο που παρήχθη από λογισμικό που κανείς δεν μπορεί πλέον να κατονομάσει
Για να φτάσουμε εκεί χρειάστηκαν περισσότεροι μηχανισμοί από ό,τι υποδηλώνουν οι δύο υπογραφές, επειδή ένα PDF δεν αποθηκεύει κείμενο με τον τρόπο που το κάνει ένα αρχείο κειμένου. Αυτό το άρθρο περιγράφει και τις δύο λειτουργίες εξαγωγής, και στη συνέχεια ανοίγει το καπό στα τρία υποκείμενα μέρη — τον αναγνώστη CMap, τον διερμηνέα ροής περιεχομένου (content stream interpreter) και την εναλλακτική αλυσίδα αποκωδικοποίησης γραμματοσειρών — επειδή η γνώση του τρόπου λειτουργίας της αντιστοίχισης είναι η διαφορά μεταξύ της αποδοχής ενός άχρηστου αποτελέσματος και της διάγνωσής του
Γιατί η εξαγωγή κειμένου είναι πιο δύσκολη από την ανάγνωση συμβολοσειρών από το αρχείο;
Μια ροή περιεχομένου (content stream) PDF καταγράφει κωδικούς χαρακτήρων, όχι χαρακτήρες. Οι τελεστές Tj και TJ (ISO 32000-1 §9.4.3) μεταφέρουν συμβολοσειρές bytes των οποίων η σημασία εξαρτάται εξ ολοκλήρου από τη γραμματοσειρά που επιλέχθηκε από την προηγούμενη Tf: το byte 0x41 μπορεί να είναι το γράμμα A υπό WinAnsi, μια αυθαίρετη γλύφος σε μια γραμματοσειρά υποσυνόλου, ή το μισό ενός CID δύο byte σε μια σύνθετη γραμματοσειρά CJK. ISO 32000-1 §9.10 ορίζει την εξαγωγή κειμένου ως ακριβώς αυτό το πρόβλημα αποκωδικοποίησης — την αντιστοίχιση κάθε κωδικού πίσω σε Unicode χρησιμοποιώντας οποιαδήποτε πληροφορία παρέχει το λεξικό της γραμματοσειράς — και το πρότυπο είναι σαφές ότι ένα συμμορφούμενο αρχείο δεν απαιτείται να παρέχει αρκετές πληροφορίες για να το κάνει αυτό
Αυτή η τελευταία πρόταση εξηγεί κάθε αναφορά σφάλματος τύπου "γιατί η αντιγραφή-επικόλληση από αυτό το PDF παράγει ακαταλαβίστικα" που έχετε δει ποτέ. Ένας παραγωγός που ενσωματώνει μια γραμματοσειρά υποσυνόλου χωρίς πίνακα /ToUnicode έχει γράψει ένα αρχείο που αποδίδεται τέλεια και εξάγεται ως ανοησία, επειδή η αντιστοίχιση κωδικού-προς-γλύφο υπάρχει αλλά η αντιστοίχιση κωδικού-προς-Unicode δεν στάλθηκε ποτέ. Οποιοδήποτε αξιόπιστο API εξαγωγής είναι επομένως μια αλυσίδα εναλλακτικών λύσεων βέλτιστης προσπάθειας, και το χρήσιμο ερώτημα είναι πόσο βαθιά φτάνει αυτή η αλυσίδα
Εξαγωγή ροής ανάγνωσης με το ExtractLoadedPageText
Για ευρετηρίαση αναζήτησης, αντιστοίχιση λέξεων-κλειδιών ή τροφοδοσία κειμένου σε μια ροή ανάλυσης, η ExtractLoadedPageText είναι η κλήση που θέλετε. Η υπογραφή είναι function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — οι δείκτες σελίδας ξεκινούν από το μηδέν, το αποτέλεσμα φτάνει ως εγγενής UnicodeString του Delphi, και η συνάρτηση επιστρέφει False όταν η σελίδα δεν έχει αναγνώσιμη ροή περιεχομένου αντί να εγείρει εξαίρεση
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
Οι αλλαγές γραμμής στο αποτέλεσμα προέρχονται από μια σκόπιμα απλή ευρετική μέθοδο: όταν η κατακόρυφη προέλευση μιας γλύφου μετακινείται κατά περισσότερο από το μισό του τρέχοντος μεγέθους γραμματοσειράς — το χαρακτηριστικό ενός βήματος Td ή T* στη ροή περιεχομένου — εισάγεται μια νέα γραμμή. Οι χαρακτήρες που ο αποκωδικοποιητής δεν μπορεί να επιλύσει γίνονται κενά διαστήματα αντί να εξαφανιστούν, οπότε τα όρια των λέξεων επιβιώνουν ακόμα και όταν μεμονωμένες γλύφοι δεν τα καταφέρνουν. Αυτό που δεν επιχειρεί αυτή η λειτουργία είναι η ομαδοποίηση σειράς ανάγνωσης ή η ανίχνευση πολλαπλών στηλών: μια σελίδα δύο στηλών εξάγεται παρεμβαλλόμενη με τη σειρά της ροής περιεχομένου, η οποία είναι συνήθως αλλά όχι πάντα η οπτική σειρά
Πότε πρέπει να χρησιμοποιήσετε την εξαγωγή με διατήρηση διάταξης;
Η ExtractLoadedPageTextLayout είναι η σωστή κλήση κάθε φορά που η θέση έχει σημασία: πίνακες, φόρμες, λίστες κώδικα, οτιδήποτε σκοπεύετε να συγκρίνετε (diff), να αναζητήσετε (grep) ή να αναλύσετε ανά στήλη. Αντί να επιπεδώνει τις γλύφες σε μια ροή, τις ομαδοποιεί σε γραμμές βάσης (baselines), ταξινομεί κάθε γραμμή βάσης ως προς τον άξονα X, και αναπαράγει τον οριζόντιο και κατακόρυφο κενό χώρο σε ένα πλέγμα χαρακτήρων σταθερού πλάτους (monospaced) με μέγεθος που προκύπτει από τη διάμεση προώθηση γλύφου και το μέγεθος της γραμματοσειράς. Τα μεγάλα κενά μεταξύ τμημάτων στην ίδια γραμμή βάσης γίνονται ακολουθίες κενών· τα μεγάλα κενά μεταξύ των γραμμών βάσης γίνονται κενές γραμμές. Το αποτέλεσμα διαβάζεται όπως φαίνεται η σελίδα
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Οι δύο λειτουργίες μοιράζονται κάθε byte του μηχανισμού αποκωδικοποίησης και διαφέρουν μόνο στον τρόπο με τον οποίο διευθετούν τις αποκωδικοποιημένες γλύφες, οπότε η επιλογή δεν κοστίζει τίποτα σε πιστότητα. Επιλέξτε την ExtractLoadedPageText όταν έχουν σημασία μόνο οι λέξεις και την ExtractLoadedPageTextLayout όταν έχει σημασία η διάταξη. Η ανίχνευση της σειράς ανάγνωσης πολλαπλών στηλών παραμένει εκτός σκοπού και για τις δύο — μια απόδοση πλέγματος μιας σελίδας δύο στηλών σάς δείχνει και τις δύο στήλες δίπλα-δίπλα, πιστά, κάτι που για τη σύγκριση είναι απολύτως σωστό, αλλά για τη ροή πεζού λόγου (prose re-flow) δεν είναι
Πώς αποκωδικοποιεί το HotPDF τους κωδικούς χαρακτήρων σε Unicode;
Το HotPDF Component επιλύει κάθε κωδικό χαρακτήρα μέσω μιας αλυσίδας εναλλακτικών λύσεων με σειρά προτεραιότητας: πρώτα τον ενσωματωμένο πίνακα /ToUnicode CMap της γραμματοσειράς, στη συνέχεια την καταχώριση /Encoding (ροή ή επώνυμο CMap), μετά — για σύνθετες γραμματοσειρές — τα πρότυπα αρχεία CMap της Adobe για συλλογές χαρακτήρων όπως Adobe-GB1, Adobe-CNS1, Adobe-Japan1 και Adobe-KR, και τέλος τους ενσωματωμένους πίνακες WinAnsi και MacRoman για απλές γραμματοσειρές. Μια στρατηγική που δεν μπορεί να δώσει απάντηση υποβαθμίζεται σιωπηρά στην επόμενη αντί να εγείρει σφάλμα, και ένας κωδικός που εξαντλεί ολόκληρη την αλυσίδα επιλύεται σε 0 ώστε ο καλών να μπορεί να μετράει τις αποτυχίες αντί να μαντεύει
Ο πίνακας /ToUnicode CMap (ISO 32000-1 §9.10.3) βρίσκεται πρώτος επειδή είναι η αντιστοίχιση που έγραψε ο παραγωγός ειδικά για την εξαγωγή. Η διαδρομή των πρότυπων CMap της Adobe έχει σημασία για έγγραφα CJK που χρησιμοποιούν προκαθορισμένα CMaps όπως το UniGB-UTF16-H αντί να ενσωματώνουν οτιδήποτε: το HotPDF αποστέλλει τα αρχεία συλλογής στον κατάλογό του resources\CMap, τα εντοπίζει σε σχέση με το εκτελέσιμο κατά το χρόνο εκτέλεσης, και αποθηκεύει προσωρινά (caches) κάθε αναλυμένο χάρτη ανά διεργασία — κάτι που αξίζει να γνωρίζετε επειδή ο μεγαλύτερος από αυτούς, ο χάρτης Adobe-GB1, είναι περίπου 2 MB κειμένου προέλευσης που δεν θέλετε να αναλύετε ξανά ανά σελίδα. Εάν ο κατάλογος απουσιάζει, ο αποκωδικοποιητής απλώς παρακάμπτει τα CMaps που βασίζονται στο δίσκο και εργάζεται με τους ενσωματωμένους πίνακες συν τις ενσωματωμένες κωδικοποιήσεις. Αυτός είναι ο καθρέφτης της πλευράς ανάγνωσης του προβλήματος διαμόρφωσης που καλύπτεται στο άρθρο διαμόρφωση σύνθετου κειμένου με το HotPDF, όπου η ίδια διάκριση κώδικα έναντι γλύφου αντιμετωπίζεται κατά τη συγγραφή
Δύο παγίδες σύνταξης CMap που αξίζει να γνωρίζετε
Τα αρχεία CMap φαίνονται εύκολα στην ανάλυση αλλά δεν είναι, και δύο λεπτομέρειες ευθύνονται για τις περισσότερες αποτυχίες αναλυτών κατά την πρώτη προσπάθεια. Η πρώτη είναι ότι ο αριθμός εγγραφών βρίσκεται πριν από τη λέξη-κλειδί της ενότητας: μια ενότητα γράφει 2 beginbfchar, όχι beginbfchar 2. Ένας αναλυτής που αναμένει τον αριθμό μετά τη λέξη-κλειδί καταναλώνει τον αριθμό ως αδέσποτο διακριτικό (token) και στη συνέχεια βρίσκει μηδενικές καταχωρίσεις σε κάθε ενότητα. Η ισχυρή προσέγγιση — αυτή στην οποία κατέληξε ο αναγνώστης του HotPDF — είναι να αγνοεί εντελώς τον αριθμό και να εκτελεί βρόχο μέχρι την αντίστοιχη λέξη-κλειδί endbfchar / endbfrange, κάτι που έχει το πλεονέκτημα να ανέχεται αρχεία του πραγματικού κόσμου των οποίων οι μετρήσεις είναι απλώς λανθασμένες
Η δεύτερη παγίδα είναι ότι οι στόχοι των bfchar και bfrange είναι συμβολοσειρές UTF-16BE, όχι ακέραιοι. Ο προορισμός <D83DDE00> σημαίνει U+1F600 — ένα υποκατάστατο ζεύγος (surrogate pair) που πρέπει να επανασυνδυαστεί σε ένα μόνο σημείο κώδικα — και η ανάγνωση αυτών των τεσσάρων byte ως ακέραιου big-endian παράγει μια άχρηστη τιμή για κάθε σημείο κώδικα εκτός του Βασικού Πολυγλωσσικού Επιπέδου (Basic Multilingual Plane). Τα emoji στα PDF δεν είναι πλέον εξωτικά, οπότε ένας αποκωδικοποιητής που παρακάμπτει τον επανασυνδυασμό υποκατάστατων αποτυγχάνει σε αρχεία που έχουν πραγματικά οι χρήστες σας. Το HotPDF αναλύει το δεκαεξαδικό λεκτικό σε ακατέργαστα bytes πρώτα, και στη συνέχεια επανασυνδυάζει τις μονάδες κώδικα UTF-16BE, γεγονός που καλύπτει επίσης τους στόχους πολλαπλών χαρακτήρων που παράγουν οι αντιστοιχίσεις συμπλεγμάτων (ligature mappings)
Κάθοδος στο επίπεδο γλύφων με το ExtractLoadedPageGlyphs
Και οι δύο κλήσεις κειμένου βασίζονται στην ExtractLoadedPageGlyphs, και ο υποκείμενος πίνακας THPDFGlyphArray είναι επίσης διαθέσιμος στον κώδικά σας. Κάθε εγγραφή THPDFGlyphRecord μεταφέρει το επιλυμένο σημείο κώδικα Unicode μαζί με τον ακατέργαστο κωδικό χαρακτήρα, το πλάτος byte του κώδικα (1, 2 ή 4, που αποφασίζεται από το codespacerange του CMap), το κλειδί και το μέγεθος του ενεργού πόρου γραμματοσειράς, την προέλευση X και Y στο χώρο του χρήστη (user-space), και την οριζόντια προώθηση. Αυτό είναι αρκετό για να δημιουργήσετε ανίχνευση ορίων λέξεων, τοποθετημένη επισήμανση ή έναν προσαρμοσμένο αλγόριθμο διάταξης χωρίς να αγγίξετε οι ίδιοι τη ροή περιεχομένου
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Η καταμέτρηση των εγγραφών Unicode = 0, όπως παραπάνω, είναι ο ειλικρινής τρόπος μέτρησης της ποιότητας εξαγωγής σε ένα δεδομένο έγγραφο προτού εμπιστευτείτε το κείμενο στη συνέχεια. Οι εγγραφές γλύφων αγκυροβολούν επίσης κάθε χαρακτήρα στον τελεστέο προέλευσης στη ροή περιεχομένου, γεγονός που καθιστά δυνατή την αναζήτηση και αντικατάσταση κειμένου φορτωμένου εγγράφου του HotPDF πάνω στην ίδια βάση
Ποια PDF δεν θα παραδώσουν το κείμενό τους;
Ορισμένα αρχεία νικούν οποιονδήποτε εξαγωγέα, και είναι καλύτερο να τα εντοπίζετε παρά να παραδίδετε το αποτέλεσμά τους. Τα σαρωμένα έγγραφα είναι η πιο ξεκάθαρη περίπτωση: μια σελίδα που είναι μια μεγάλη εικόνα δεν περιέχει καθόλου τελεστές κειμένου, οπότε η εξαγωγή επιστρέφει σωστά μια κενή συμβολοσειρά — η λύση είναι το OCR, και η εξαγωγή των εικόνων της σελίδας από το φορτωμένο PDF είναι το πρώτο βήμα αυτής της ροής εργασίας. Οι γραμματοσειρές υποσυνόλου χωρίς πίνακα /ToUnicode είναι η πιο δύσκολη περίπτωση: εάν η διαδρομή /Encoding και τα πρότυπα CMaps εμφανιστούν επίσης κενά, αυτές οι γλύφοι επιλύονται σε 0 και εμφανίζονται ως κενά διαστήματα στις κλήσεις κειμένου. Τα κρυπτογραφημένα έγγραφα εξάγονται κανονικά, με την προϋπόθεση ότι τα φορτώνετε με τον κωδικό πρόσβασής τους μέσω της υπερφόρτωσης LoadFromFile, έτσι ώστε οι ροές να αποκρυπτογραφούνται πριν τις δει ο διερμηνέας
Ένα στενότερο όριο αξίζει να αναφερθεί με σαφήνεια: η αλυσίδα αποκωδικοποίησης διαβάζει τις ροές CMap και περιεχομένου μέσω της διαδρομής Flate του HotPDF, οπότε μια γραμματοσειρά της οποίας η ροή ToUnicode χρησιμοποιεί ένα ασυνήθιστο φίλτρο υποβαθμίζεται στην επόμενη στρατηγική αντί να αποτύχει η σελίδα. Στην πράξη, το FlateDecode καλύπτει σχεδόν οτιδήποτε έχει παραχθεί τις τελευταίες δύο δεκαετίες, και η υποβάθμιση είναι σιωπηρή από σχεδιασμό — παίρνετε το καλύτερο κείμενο που επιτρέπει το αρχείο αντί για μια εξαίρεση. Ο ίδιος μηχανισμός αντικειμένων στην πλευρά της ανάγνωσης που επιλύει τα λεξικά γραμματοσειρών εδώ τροφοδοτεί επίσης την επεξεργασία μεταδεδομένων σε φορτωμένα έγγραφα, έτσι ώστε μια ροή εργασίας εισαγωγής εγγράφων να μπορεί να εξαγάγει, να επιθεωρήσει και να προσθέσει σχόλια σε ένα πέρασμα
Η εξαγωγή κειμένου, η απόδοση με διατήρηση διάταξης, η πρόσβαση σε επίπεδο γλύφων, και οι δυνατότητες αναζήτησης και αντικατάστασης που βασίζονται σε αυτά αποτελούν μέρος του τυπικού HotPDF Component για Delphi και C++Builder — χωρίς εξωτερικά DLLs, χωρίς υπηρεσίες κειμένου του λειτουργικού συστήματος, απλώς Object Pascal που μπορείτε να εντοπίσετε βήμα-βήμα όταν ένα περίεργο αρχείο προσγειώνεται στην ουρά σας