Το HotPDF Delphi Component ξαναχτίζει κενά λέξεων και αλλαγές γραμμής στο THotPDF.ExtractLoadedPageText από τη γεωμετρία των glyphs, όχι από χαρακτήρες κενού. Ένα κενό μπαίνει όταν το διάκενο μετά το ίδιο πλάτος ενός glyph ξεπερνά το 0.15 του ύψους του κειμένου, και νέα γραμμή ξεκινά μόνο όταν η αρχή του κειμένου μετακινείται κατά μήκος της κατεύθυνσης γραφής πάνω από το μισό ύψος του κειμένου. Από το v2.768.3 το κείμενο της σελίδας περιλαμβάνει επίσης κείμενο ζωγραφισμένο μέσω Form XObjects και αφήνει έξω glyphs έξω από την ορατή περιοχή crop. Η υπόλοιπη ανάλυση εξηγεί γιατί κάθε κανόνας μοιάζει όπως μοιάζει, γιατί καθένας τους αντικατέστησε έναν πιο απλό κανόνα που έδινε πειστική αλλά λάθος έξοδο σε πραγματικά έγγραφα
Τα συμπτώματα είναι γνώριμα σε όποιον έχει ταΐσει κείμενο PDF σε ευρετήριο αναζήτησης. Μια σελίδα εξώφυλλου εξάγεται ως PDFReferenceManualNovember4,1998, μια φορολογική φόρμα χωρίζεται σε 156 γραμμές, ένα διαγώνιο watermark έρχεται ένας χαρακτήρας ανά γραμμή, και ένα κομμένο proof αρχίζει με τη slug line του εκτυπωτή που κανένας viewer δεν δείχνει ποτέ. Κανένα από αυτά τα αρχεία δεν είναι χαλασμένο. Το καθένα χρησιμοποιεί έναν απόλυτα νόμιμο τρόπο τοποθέτησης κειμένου που ένας αφελής extractor διαβάζει λάθος
Γιατί το εξαγμένο κείμενο PDF χάνει τα κενά λέξεων;
Το εξαγμένο κείμενο χάνει τα κενά λέξεων επειδή ένα PDF δεν απαιτείται ποτέ να τα περιέχει. Ένας producer μπορεί να χωρίζει λέξεις δείχνοντας χαρακτήρα κενού, αλλά μπορεί εξίσου να μετακινήσει την πένα με έναν αριθμό μέσα σε πίνακα TJ (ISO 32000-1 §9.4.3) ή με φρέσκο Td (§9.4.2), και η έξοδος TeX, πολλά αρχεία Distiller και οι περισσότερες justified διατάξεις κάνουν ακριβώς αυτό. Πριν το v2.766.76, το HPDFAssemblePageText κοιτούσε μόνο την κάθετη μετακίνηση, οπότε ένας διαχωρισμός λέξεων που γινόταν με τοποθέτηση εξαφανιζόταν απλώς. Ο assembler τώρα μετρά, κατά την κατεύθυνση γραφής του προηγούμενου glyph, την απόσταση από το τέλος του ίδιου πλάτους εκείνου του glyph ως την αρχή του τρέχοντος glyph, και εισάγει ένα κενό όταν η απόσταση υπερβαίνει το 0.15 του ύψους box του τρέχοντος glyph, μετρημένο από ascent σε descent σε user space. Κανένα κενό δεν προστίθεται όταν η μία πλευρά είναι ήδη κενή, και κανένα ανάμεσα σε δύο CJK χαρακτήρες, επειδή η justification απλώνει τα ideographs χωρίς εκείνο το τέντωμα να σημαίνει όριο λέξης. Οι εγγραφές glyphs εκθέτουν την ίδια γεωμετρία, οπότε μπορείτε να αναπαραγάγετε την απόφαση όταν ένα συγκεκριμένο αρχείο σας προβληματίζει
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// ύψος ascent-έως-descent του box του glyph, σε user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// οριζόντιο κείμενο: κενό από το τέλος του ίδιου πλάτους του προηγούμενου glyph
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
Γιατί μετράμε από το ίδιο πλάτος του glyph αντί για τη θέση της πένας;
Το HotPDF μετρά τα κενά λέξεων από GlyphEndX / GlyphEndY επειδή η θέση της πένας μετά από ένα glyph περιέχει ήδη διάσταση που δεν είναι κενό. Το ISO 32000-1 §9.4.4 ορίζει την οριζόντια μετατόπιση ως το πλάτος του glyph επί το μέγεθος της γραμματοσειράς, συν character spacing Tc, συν word spacing Tw, όλα κλιμακωμένα με Tz. Τα BaselineEndX / BaselineEndY κρατούν εκείνη την πλήρη μετατόπιση, ενώ τα GlyphEndX / GlyphEndY κρατούν μόνο το advance της γραμματοσειράς και το Tz. Η διαφορά μετράει για producers που σφίγγουν το tracking με αρνητικό Tc και μετά επιστρέφουν το κενό μέσω μιας προσαρμογής TJ μετά από κάθε glyph: μετρημένο από τη θέση της πένας, η επιστροφή μοιάζει με κενό, και ο κινεζικός όρος «95后» εξαγόταν ως «9 5 后». Το κατώφλι είναι δεμένο με το ύψος box του glyph αντί για το μέγεθος Tf για παρόμοιο λόγο. Τα exports του Word συχνά γράφουν 1 Tf και κουβαλούν το πραγματικό μέγεθος σε έναν κλιμακωμένο Tm, οπότε το Tfs λέει 1 ενώ το κείμενο έχει ύψος 10 points, και ένας κανόνας κλειδωμένος στο Tfs θα μεταχειριζόταν διαφορετικά τις δύο συλλαβές της ίδιας σελίδας
Ο κανόνας έχει ειλικρινή όρια. Μια επικεφαλίδα στοιθεμένη με πολύ χαλαρό tracking, όπου το Tc μόνο του ανοίγει πάνω από 0.15 του ύψους του κειμένου ανάμεσα στα γράμματα, εξάγεται με κενό ανάμεσα σε κάθε γράμμα, που είναι ό,τι δείχνει η σελίδα αλλά μάλλον όχι ό,τι θέλατε να ευρετηριάσετε. Κομμάτια σχεδιασμένα εκτός σειράς σε μια baseline παράγουν αρνητικό διάκενο και ενώνονται χωρίς κενό. Καμία από τις δύο περιπτώσεις δεν είναι συχνή σε κείμενο σώματος, και σε ένα test corpus η αλλαγή ανέβασε τις αντιστοιχίες λέξεων έναντι ενός reference extractor σε 28 σελίδες χωρίς να χαμηλώσει καμία
Πότε ξεκινά το HotPDF νέα γραμμή στο εξαγμένο κείμενο;
Από το v2.766.79, νέα γραμμή ξεκινά όταν η μετακίνηση από την αρχή του προηγούμενου glyph σε εκείνη του τρέχοντος, προβαλλόμενη πάνω στην κάθετο της προηγούμενης κατεύθυνσης γραφής, υπερβαίνει το μισό από το μεγαλύτερο ύψος box των δύο glyphs. Ο παλιότερος κανόνας σύγκρινε την ακατέργαστη μετακίνηση Y με το μισό του Tfs, που απέτυχε σε δύο κατευθύνσεις. Με 1 Tf και κλιμακωμένο Tm το κατώφλι συρρικνώθηκε σε μισή μονάδα, οπότε ένα superscript σηκωμένο με text rise 0.4 ή συνηθισμένος jitter baseline έσπασε τη γραμμή. Ο κανόνας αγνοούσε επίσης τελείως το X, οπότε κείμενο κάτω από γυρισμένο Tm κατέβαινε τη σελίδα με κάθε glyph και βγηνόταν ένα glyph ανά γραμμή. Η προβολή πάνω στην κάθετο της κατεύθυνσης κάνει τα γυρισμένα runs να συμπεριφέρονται ως οριζόντια, και το να παίρνεται το μεγαλύτερο από τα δύο ύψη κρατά μια μεγάλη λέξη δείγματος και τη μικρή λεζάντα της σε μία γραμμή όταν μοιράζονται baseline. Στη φορολογική φόρμα που αναφέρθηκε παραπάνω, ο αριθμός γραμμών έπεσε από 156 σε 97. Το κάθετο κείμενο σε writing mode 1 (§9.7.4.3) ακολουθεί ξεχωριστή διαδρομή: εκείνα τα glyphs ομαδοποιούνται σε στήλες, διαβάζονται από δεξιά προς αριστερά και από πάνω προς κάτω, με αλλαγή γραμμής σε κάθε αλλαγή στήλης
Ποιο κείμενο περιλαμβάνει ή αφήνει έξω το ExtractLoadedPageText;
Το ExtractLoadedPageText επιστρέφει το κείμενο που δείχνει ένας viewer. Από το v2.766.80 δουλεύει από τα ορατά glyphs μόνο, ρίχνοντας κάθε glyph του οποίου το κέντρο box πέφτει έξω από το GetLoadedPageVisibleBox, που είναι το CropBox κομμένο στο MediaBox (§14.11.2). Αυτό αφαιρεί slug lines και άλλα σημάδια του εκτυπωτή στοιθεμένα ως κείμενο έξω από την περιοχή κοπής. Το ExtractLoadedPageGlyphs σκόπιμα συνεχίζει να επιστρέφει κάθε glyph του content stream της σελίδας, οπότε μπορείτε ακόμα να βρείτε εκείνο το υλικό όταν το χρειάζεστε. Το φίλτρο είναι ένα box τεστ, όχι ένα τεστ ορατότητας: κείμενο κρυμμένο από clipping path, ζωγραφισμένο σε λευκό ή καλυμμένο από εικόνα εξάγεται ακόμα
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// κάθε glyph του content stream της σελίδας, slug line συμπεριλαμβανομένου
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// μόνο ό,τι δείχνει η σελίδα, με κείμενο Form XObject ενωμένο
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Κείμενο ζωγραφισμένο μέσω Form XObjects είναι μέρος του κειμένου της σελίδας από το v2.768.3. Headers, σφραγίδες και watermarks μένουν πολύ συχνά σε forms, και μερικά έγγραφα προτύπων έχαναν 30 έως 35 τοις εκατό των χαρακτήρων τους πριν την αλλαγή. Το THotPDF.InterpretContentWithForms καταγράφει κάθε Do μαζί με τον CTM που ισχύει, ερμηνεύει τη φόρμα στο /Matrix της επί εκείνον τον CTM (§8.10.1), και ενώνει τα glyphs της φόρμας στη θέση του Do, κάνοντας αναδρομή σε εμφωλευμένες φόρμες. Μια φόρμα χωρίς δικό της /Resources δανείζεται εκείνα του stream που τη ζωγραφίζει, όπως επιτρέπει το §7.8.3. Τα glyphs φόρμας κουβαλούν TokenIndex = -1, και το ExtractLoadedPageGlyphs εξακολουθεί να επιστρέφει μόνο glyphs του page stream, επειδή αναζήτηση, αντικατάσταση και redaction γράφουν αλλαγές πίσω μέσω TokenIndex και θα επεξεργάζονταν λάθος bytes αν γλιστρούσε μέσα glyph φόρμας. Δύο απλοποιήσεις αξίζει να ξέρετε: το κείμενο φόρμας δεν κόβεται στο /BBox της φόρμας, και η αναδρομή σταματά στα 12 επίπεδα αντί μέσω ανίχνευσης κύκλων, οπότε μια δυσμόρφωτη φόρμα που ζωγραφίζει τον εαυτό της επαναλαμβάνει το κείμενό της μέχρι να φτάσει εκείνο το όριο
Γιατί κείμενο μετά από τελεστή Q αποκωδικοποιούνταν ως σκουπίδια;
Κείμενο μετά από Q μπορούσε να αποκωδικοποιηθεί λάθος πριν το v2.766.73 επειδή ο extractor αποθήκευε μόνο τον CTM πάνω σε q. Οι παράμετροι κατάστασης κειμένου, δηλαδή γραμματοσειρά, μέγεθος, Tc, Tw, Tz, TL, mode rendering και rise, ανήκουν στη graphics state (§9.3.1), οπότε το Q πρέπει να τις επαναφέρει μαζί με όλα τα άλλα στη στοίβα (§8.4.2). Μια industry αναφορά επέλεγε μια γραμματοσειρά δύο bytes Identity-H μέσα σε q … Q και μετά έδειχνε κείμενο WinAnsi ενός byte χωρίς δικό της Tf. Ο extractor κρατούσε την εσωτερική γραμματοσειρά, διάβαζε τους οδηγούς και τη λέξη «Adobe» στον πίνακα περιεχομένων ως κωδικούς δύο bytes, και έχανε το 15% των χαρακτήρων της σελίδας. Η στοίβα q/Q του interpreter κρατά πλέον πλήρη κατάσταση κειμένου. Οι κανόνες εξαγωγής που περιγράφονται εδώ ισχύουν για κάθε σελίδα, οπότε ένα ολόκληρο έγγραφο πάει σε αρχείο σε μία κλήση
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// κενό εύρος = κάθε σελίδα· form feed ανάμεσα σε σελίδες· UTF-8 BOM
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Ποιο text API του HotPDF να χρησιμοποιήσετε;
Το ExtractLoadedPageText μένει στη σειρά του content stream, που είναι το σωστό default για αναζήτηση και ευρετηρίαση· η αλυσίδα αποκωδικοποίησης από κάτω του καλύπτεται στο εξαγωγή κειμένου από φορτωμένα PDF με το HotPDF. Για tagged έγγραφα όπου η σειρά συγγραφής μετράει, η εξαγωγή κειμένου με σειρά δομής περπατά το δέντρο δομής αντί να μαντεύει από τη γεωμετρία, και για δεδομένα κλειδωμένα σε πίνακες, η εξαγωγή τυποποιημένων πινάκων across σελίδες επιστρέφει κελιά αντί για γραμμές. Πλήρης αναφορά API και trial download βρίσκονται στη σελίδα προϊόντος HotPDF Delphi PDF Component