Η εξαγωγή κειμένου, εικόνων και γραμματοσειρών από ένα υπάρχον PDF ακούγεται λυμένο πρόβλημα μέχρι να περάσετε από μέσα ένα πραγματικό σώμα εγγράφων. Στρέψτε έναν indexer αναζήτησης σε σαράντα χιλιάδες αρχεία πελατών και οι αστοχίες ταξινομούνται σε λίγους αναγνωρίσιμους σωρούς. Λέξεις κολλούν μεταξύ τους επειδή κανείς δεν είπε στον extractor πόσο πλατύ κενό μετράει ως διάστημα. Άλλες σελίδες επιστρέφουν ακαταλαβίστικα επειδή μια subsetted γραμματοσειρά δεν φέρει καμία αντιστοίχιση από τους κωδικούς glyph της σε πραγματικούς χαρακτήρες. Και «το λογότυπο της εταιρείας» αποδεικνύεται ότι είναι εννέα ξεχωριστά αντικείμενα εικόνας στοιβαγμένα πίσω από ένα soft mask. Τίποτα από αυτά δεν είναι σφάλμα της βιβλιοθήκης. Είναι η διαφορά ανάμεσα στο να καλείτε μια συνάρτηση εξαγωγής και στο να καταλαβαίνετε τι μπορεί και τι δεν μπορεί να ανακτήσει αυτή η συνάρτηση από τα bytes στον δίσκο
Η losLab PDF Library, στην έκδοση για Pascal, δίνει σε κώδικα Delphi και C++Builder περισσότερους από έναν τρόπους να διαβάσει καθένα από αυτά τα τρία ρεύματα, και τα επίπεδα διαφέρουν σε αυτό που εγγυώνται. Το κόλπο είναι να ταιριάξετε το επίπεδο με τη δουλειά: ένα ευρετήριο αναζήτησης, ένας ελεγκτής απαλοιφής και ένα πέρασμα preflight PDF/A θέλουν όλα διαφορετικά πράγματα από την ίδια σελίδα, και το να καταφύγετε σε λάθος κλήση σπαταλά κόπο ή παράγει έξοδο που δεν μπορείτε να εμπιστευτείτε
Επίπεδα εξαγωγής κειμένου και τι υπόσχεται το καθένα
Το GetPageText δέχεται τιμή επιλογών από 0 έως 8, και αυτός ο αριθμός επιλέγει μηχανή και όχι μορφή. Οι τιμές 0 έως 2 τρέχουν ένα ελαφρύ πέρασμα που είναι μια χαρά για γρήγορη προεπισκόπηση. Οι τιμές 3 έως 8 περνούν από τη μηχανή που αντιλαμβάνεται τη διάταξη, η οποία ξαναχτίζει γραμμές και αποστάσεις από το πού κάθονται πραγματικά τα glyphs στη σελίδα. Μέσα σε αυτό το εύρος οι παραλλαγές μετράνε: το 4 και το 6 σπάνε την έξοδο σε λέξεις, το 5 και το 6 εκπέμπουν πλάτη ανά glyph, και το 7 επιστρέφει καθαρό κείμενο με τη γραμματοσειρά, το χρώμα και τα metadata των μπλοκ σκόπιμα πεταμένα. Η επιλογή 7 είναι εκείνη που δίνετε σε ένα ευρετήριο αναζήτησης, αφού το ευρετήριο θέλει λέξεις και τίποτε άλλο
Καμία ρύθμιση επιλογών δεν μπορεί να σώσει ένα έγγραφο που δεν κουβαλούσε εξαρχής την πληροφορία. Το PDF αντιστοιχίζει κωδικούς χαρακτήρων σε σχήματα glyph, και το μόνο που αντιστοιχίζει αυτούς τους κωδικούς πίσω σε αναγνώσιμο κείμενο είναι το ToUnicode CMap μιας γραμματοσειράς (ISO 32000-1 §9.10). Όταν μια subsetted γραμματοσειρά έρχεται χωρίς αυτό, κάθε extractor κολλάει. Αυτή η βιβλιοθήκη, η αντιγραφή-επικόλληση σε έναν viewer, ένα ανταγωνιστικό toolkit: όλα ανάγονται στο να μαντεύουν από ονόματα glyph ή να μην επιστρέφουν τίποτα. Η πρακτική απάντηση είναι ο εντοπισμός, όχι οι ηρωισμοί. Βαθμολογήστε τη σελίδα ως χαμηλής εμπιστοσύνης και στείλτε τη σε OCR, γιατί το να ευρετηριάσετε σιωπηλά τα σκουπίδια είναι χειρότερο από το να παραδεχτείτε ότι δεν μπορείτε να τη διαβάσετε
Για τις περιπτώσεις που δεν καλύπτουν οι flat επιλογές, δηλαδή προσαρμοσμένη κατάτμηση σε tokens, ιατροδικαστική ανάλυση του content stream, ένα χωνί κειμένου χτισμένο με δικούς σας κανόνες, ο αποκωδικοποιητής είναι διαθέσιμος ένα επίπεδο πιο κάτω. Το TPDFExtractor κατασκευάζεται πάνω στο resources dictionary και στη συλλογή γραμματοσειρών μιας σελίδας. Η μέθοδός του ExtractTextW περνά ακατέργαστες λειτουργίες κειμένου του content stream ξανά μέσα από τον ίδιο μηχανισμό γραμματοσειρών για να ανακτήσει Unicode, και το συμβάν του OnFindObject σας παραδίδει κάθε αντικείμενο καθώς περνά ροϊκά. Ο περισσότερος κώδικας δεν χρειάζεται ποτέ να φτάσει τόσο βαθιά. Οι εφαρμογές που το χρειάζονται είναι εκείνες που χαίρονται που το επίπεδο αυτό είναι δημόσιο και όχι θαμμένο
Τοποθετημένα μπλοκ: η μονάδα των αποτελεσμάτων αναζήτησης και του ελέγχου απαλοιφής
Το απλό κείμενο σας λέει τι λέει η σελίδα. Αργά ή γρήγορα ένα προϊόν πρέπει επίσης να ξέρει πού το λέει, ώστε να επισημάνει ένα αποτέλεσμα αναζήτησης, να σχεδιάσει πλαίσιο γύρω από έναν υποψήφιο προς απαλοιφή ή να αγκυρώσει ένα annotation στο σωστό σημείο. Το ExtractPageTextBlocks επιστρέφει handle σε λίστα από αποσπάσματα κειμένου, και το καθένα φέρει το κείμενό του, το bounding box του και το όνομα και το μέγεθος της γραμματοσειράς με την οποία στοιχειοθετήθηκε:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
Μία λεπτομέρεια σε αυτόν τον τομέα μπερδεύει τις ενσωματώσεις περισσότερο από κάθε άλλη. Τα SetTextExtractionArea, SetTextExtractionWordGap και SetTextExtractionOptions είναι κατάσταση σε επίπεδο εγγράφου που επιμένει, όχι ορίσματα που περνάτε ανά κλήση. Ρυθμίστε έναν περιορισμό περιοχής για μία λειτουργία, ας πούμε να διαβάζετε μόνο τη ζώνη της κεφαλίδας για να ταξινομήσετε ένα έγγραφο, και θα περικόπτει σιωπηλά κάθε επόμενη εξαγωγή στο ίδιο handle, μαζί και τα επίπεδα του GetPageText που αντιλαμβάνονται τη διάταξη και στα οποία καταφεύγετε αργότερα. Είτε επαναφέρετε την κατάσταση εξαγωγής ανάμεσα σε λογικές εργασίες, είτε δώστε σε κάθε εργασία το δικό της handle εγγράφου
Το κατώφλι κενού λέξης είναι ο μοχλός για εκείνον τον πρώτο σωρό αστοχιών, τις λέξεις που κολλούν. Το SetTextExtractionWordGap λέει στη μηχανή διάταξης πόσος οριζόντιος χώρος, μετρημένος σε σχέση με τις αποστάσεις των glyphs της ίδιας της σελίδας, χωρίζει τη μία λέξη από την επόμενη. Ένας πυκνός πίνακας θέλει μικρότερο κενό από μια χαλαρά στοιχειοθετημένη διαφημιστική σελίδα, οπότε ένα κατώφλι ρυθμισμένο ανά κλάση εγγράφων νικά μια καθολική σταθερά. Επιμένει πάνω στο έγγραφο όπως και η υπόλοιπη κατάσταση εξαγωγής, οπότε σχεδιάστε να το ορίζετε συνειδητά αντί να το ρυθμίσετε μία φορά και να το ξεχάσετε
Εικόνες: αυθεντικά streams, όχι στιγμιότυπα οθόνης
Ο λάθος τρόπος να βγάλετε εικόνες από ένα PDF είναι να αποδώσετε τη σελίδα και να την περικόψετε. Αυτό αναδειγματοληπτεί τα pixels, ψήνει μέσα οποιαδήποτε περιστροφή και πετά ό,τι ήταν το πρωτότυπο. Το GetPageImageList αντίθετα απαριθμεί τους πραγματικούς πόρους εικόνας στους οποίους παραπέμπει η σελίδα, και κάθε στοιχείο επιστρέφει τις ιδιότητές του και τα αυθεντικά, ανέγγιχτα δεδομένα του:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Ελέγξτε το GetImageListItemFormatDesc προτού υποθέσετε οτιδήποτε για ένα στοιχείο, γιατί αυτό στο οποίο παραπέμπει μια σελίδα σπάνια είναι μία τακτοποιημένη εικόνα ανά ορατή εικόνα. Ένα soft mask εμφανίζεται ως δική του ξεχωριστή καταχώριση. Το ίδιο XObject συχνά επαναλαμβάνεται σε πολλές σελίδες, οπότε αφαιρέστε τα διπλότυπα με hash περιεχομένου προτού αρχειοθετήσετε μια εξαγωγή «όλων των εικόνων», αλλιώς θα γράψετε το ίδιο λογότυπο εκατό φορές. Τα JPEG σε CMYK χρειάζονται διαχείριση χρώματος κατάντη, αλλιώς αποδίδονται ανεστραμμένα σε viewers που παίρνουν τα κανάλια κατά γράμμα. Όταν θέλετε απογραφή σε επίπεδο εγγράφου αντί για μία σελίδα τη φορά, το FindImages μαζί με το SetFindImagesMode σαρώνει ολόκληρο το αρχείο με ένα πέρασμα
Υπάρχει ένα όριο που αξίζει να τεθεί στους ενδιαφερόμενους προτού γράψει κανείς κριτήρια αποδοχής: η εξαγωγή εικόνων επιστρέφει μόνο πόρους raster. Ένα λογότυπο ή γράφημα σχεδιασμένο ως διανυσματικές διαδρομές δεν είναι εικόνα με την έννοια του πόρου και δεν θα εμφανιστεί ποτέ σε καμία λίστα εικόνων, όσο καθαρά κι αν διαβάζεται ως εικόνα στην οθόνη. Όταν η απαίτηση είναι πραγματικά να παραδοθεί εκείνο το γράφημα ως αρχείο, η έντιμη προσέγγιση είναι να αποδώσετε την περιοχή της σελίδας σε bitmap, που είναι διαφορετική λειτουργία με διαφορετική πιστότητα. Τα δύο είδη εξόδου δεν ανήκουν στον ίδιο φάκελο εξαγωγής χωρίς ετικέτα που να λέει ποιο είναι ποιο
Γραμματοσειρές: επιφάνεια ελέγχου, όχι λειτουργία εξαγωγής
Το API γραμματοσειρών απαντά σε ερωτήματα για γραμματοσειρές. Δεν σας παραδίδει τα ίδια τα αρχεία γραμματοσειράς, και αυτή η διάκριση διαμορφώνει όλα όσα μπορείτε να χτίσετε πάνω του. Αφού το FindFonts σαρώσει το έγγραφο, η απαρίθμηση διατρέχει τις γραμματοσειρές κατά ID, και οι κλήσεις ιδιοτήτων αναφέρουν για όποια γραμματοσειρά είναι επιλεγμένη εκείνη τη στιγμή:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // οι δείκτες ξεκινούν από το 1, όχι το 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Προσέξτε τα όρια του βρόχου. Οι δείκτες γραμματοσειρών τρέχουν από 1 έως FontCount, ενώ οι δείκτες των μπλοκ κειμένου και της λίστας εικόνων λίγες παραγράφους πιο πάνω ξεκινούν από το μηδέν. Μεταφέρετε τη μία σύμβαση στην άλλη και παίρνετε ένα off-by-one που είτε παραλείπει την πρώτη γραμματοσειρά είτε ξεφεύγει από το τέλος, και θα περάσει από πρόχειρες δοκιμές γιατί τα περισσότερα έγγραφα έχουν αρκετές γραμματοσειρές και η λάθος εξακολουθεί να φαίνεται εύλογη. Να είστε σαφείς και για την εμβέλεια. Αυτό το API δεν έχει εξαγωγή γραμματοσειράς σε επίπεδο bytes. Καμία κλήση δεν επιστρέφει το ενσωματωμένο πρόγραμμα γραμματοσειράς ως αρχείο TTF ή OTF, και η απαρίθμηση συν η επιθεώρηση metadata είναι όλο το προβλεπόμενο μοντέλο. Αυτό το μοντέλο καλύπτει παρ' όλα αυτά όσα ζητά πραγματικά η παραγωγή από τις γραμματοσειρές: εντοπισμό subset από μοτίβο ονόματος, ελέγχους ενσωμάτωσης πριν από μια αρχειακή μετατροπή (μια μη ενσωματωμένη γραμματοσειρά είναι απόλυτο εμπόδιο για PDF/A, όπως αναλύει το preflight PDF/A και PDF/UA σε Delphi), και διαγνωστικά κωδικοποίησης για όταν πέφτει η εμπιστοσύνη στην εξαγωγή. Υπάρχει και λόγος αδειοδότησης που το όριο μπαίνει εδώ. Ένα πρόγραμμα subset γραμματοσειράς είναι αδειοδοτημένο υλικό και, καθώς του λείπουν τα περισσότερα glyphs, είναι έτσι κι αλλιώς άχρηστο ως εγκαταστάσιμη γραμματοσειρά. Το να το αντιμετωπίζετε ως metadata ελέγχου και όχι ως εξαγώγιμο περιουσιακό στοιχείο είναι η θέση που μπορείτε να υπερασπιστείτε
Εκείνη η τελευταία κλήση αξίζει τον κόπο στη διαλογή. Τρέξτε το GetFontEncoding σε κάθε γραμματοσειρά, διαβάστε το μαζί με τη σημαία subset, και μπορείτε να προβλέψετε την ποιότητα της εξαγωγής προτού τραβήξετε έναν και μόνο χαρακτήρα. Μια σελίδα της οποίας όλες οι γραμματοσειρές είναι subsetted με μη τυπικές κωδικοποιήσεις είναι υποψήφια για OCR και μόνο από την επιθεώρηση, κάτι που επιτρέπει σε μια ροή παρτίδας να τη δρομολογήσει σωστά χωρίς να σπαταλήσει πρώτα ένα αποτυχημένο πέρασμα εξαγωγής πάνω της
Εξαγωγή σε κλίμακα χωρίς φόρτωση εγγράφων
Σε μια ροή παρτίδας, η φόρτωση ολόκληρου εγγράφου μόνο και μόνο για να διαβάσετε μία σελίδα είναι σπαταλημένο I/O, και αθροίζεται γρήγορα σε ένα ολόκληρο σώμα εγγράφων. Οι παραλλαγές μίας κλήσης, τα ExtractFilePageText και ExtractFilePageTextBlocks, δέχονται απευθείας όνομα αρχείου, κωδικό και αριθμό σελίδας και παρακάμπτουν την πλήρη φόρτωση. Για αρχεία κλίμακας gigabyte υπάρχει ακόμη μια χαμηλότερη ταχύτητα. Η διαδρομή άμεσης πρόσβασης ανοίγει ένα αρχείο με streaming αναγνώσεις xref, οπότε το DAOpenFileReadOnly ακολουθούμενο από το DAExtractPageText αγγίζει μόνο τα αντικείμενα που χρειάζεται πραγματικά εκείνη η μία σελίδα. Έρχεται με μια αλλαγή σύμβασης που αξίζει να αποστηθίσετε: οι συναρτήσεις DA διευθυνσιοδοτούν σελίδες με PageRef, ένα handle αναφοράς αντικειμένου που παίρνετε από το DAFindPage, ποτέ με σκέτο αριθμό σελίδας. Περάστε τον αριθμό εκεί που ανήκει το handle και η κλήση δουλεύει πάνω σε λάθος αντικείμενο χωρίς να προκαλέσει σφάλμα, που είναι το χειρότερο είδος λάθους για αποσφαλμάτωση. Η υπόλοιπη εργαλειοθήκη άμεσης πρόσβασης παρουσιάζεται στη συγχώνευση, διαχωρισμό και άμεση πρόσβαση μεγάλων PDF
Αν υπάρχει μία μόνο συνήθεια που ξεχωρίζει τον κώδικα εξαγωγής που επιβιώνει σε ένα πραγματικό σώμα εγγράφων από εκείνον που κουτσαίνει, είναι η αντιμετώπιση της σελίδας ως μη έμπιστης εισόδου και όχι ως καθαρής πηγής δεδομένων. Κείμενο που διαφωνεί με αυτό που αποδίδει ο viewer είναι σχεδόν πάντα πρόβλημα κωδικοποίησης, ένα ligature που καταρρέει σε ένα glyph ή μια subset γραμματοσειρά που της λείπουν οι καταχωρίσεις ToUnicode, και η λύση είναι να μετράτε την εμπιστοσύνη και να εκτρέπετε τις κακές σελίδες σε OCR, όχι να παλεύετε με τα bytes. Το API γραμματοσειρών δεν πρόκειται ποτέ να παραγάγει TTF ή OTF, εκ σχεδιασμού, οπότε χτίστε τις ροές γραμματοσειρών γύρω από ερωτήματα ελέγχου. Και η επίμονη κατάσταση εξαγωγής, με πρώτο το ορθογώνιο της περιοχής, είναι ρύθμιση που σας ανήκει για όλη τη ζωή ενός handle εγγράφου, όχι παράμετρος που ξεχνάτε μετά από μία κλήση. Βάλτε σωστά αυτά τα τρία αντανακλαστικά και το υπόλοιπο API συμπεριφέρεται
Τα builds αξιολόγησης, τα demo έργα και η πλήρης αναφορά του API εξαγωγής βρίσκονται στη σελίδα προϊόντος losLab PDF Library for Delphi