Τεχνικό Άρθρο

Εξαγωγή κειμένου από αρχεία PDF με το PDFium Component στο Delphi

Η εξαγωγή κειμένου από PDF φαίνεται απλή μέχρι να συναντήσετε ένα έγγραφο όπου το επίπεδο κειμένου απουσιάζει, είναι κατεστραμμένο ή χωρισμένο σε δεκάδες μικροσκοπικές σειρές χαρακτήρων χωρίς ουσιαστική σειρά. Το PDFium Component σάς δίνει δύο σημεία εισόδου: τον πίνακα Character[] για ακατέργαστη πρόσβαση βάσει δείκτη σε κάθε γλύφο μιας σελίδας, και το ReadablePageContent για μια δομημένη προβολή που ανακατασκευάζει παραγράφους και επικεφαλίδες από το δέντρο ετικετών του PDF ή από ευρετική ανάλυση. Κανένα από τα δύο δεν είναι πάντα η σωστή επιλογή, επομένως η κατανόηση του τι εκθέτει το καθένα έχει σημασία

Άνοιγμα του εγγράφου και η παγίδα της σιωπηλής αποτυχίας

Το TPdf ανοίγει ένα αρχείο ρυθμίζοντας το FileName και αλλάζοντας το Active := True. Η κρίσιμη λεπτομέρεια: το Active := True δεν προκαλεί ποτέ εξαίρεση. Εάν το αρχείο λείπει, προστατεύεται με κωδικό πρόσβασης ή είναι κατεστραμμένο, το PDFium συλλαμβάνει το σφάλμα εσωτερικά και το Active παραμένει απλώς False. Αυτό σημαίνει ότι κάθε βρόχος εξαγωγής πρέπει να προστατεύεται από αυτό:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Τα προστατευμένα με κωδικό πρόσβασης αρχεία χρειάζονται τη ρύθμιση Pdf.Password := '...' πριν από το Active := True. Δεν υπάρχει δεύτερη ευκαιρία: μόλις το Active αποτύχει, κλείνετε και ανοίγετε ξανά με τον σωστό κωδικό πρόσβαση

Εξαγωγή σελίδα προς σελίδα με το Character[]

Η προσέγγιση χαμηλότερου επιπέδου διατρέχει κάθε χαρακτήρα σε κάθε σελίδα. Ρυθμίστε το Pdf.PageNumber για να φορτώσετε το επίπεδο κειμένου για αυτήν τη σελίδα, και στη συνέχεια επαναλάβετε τις καταχωρήσεις CharacterCount χρησιμοποιώντας την ιδιότητα Character[]. Αξίζει να ελέγξετε δύο σημαίες σε κάθε καταχώρηση: η CharacterGenerated[i] επισημαίνει συνθετικούς γλύφους που εισάγονται από τη μηχανή απόδοσης (μαλακά ενωτικά σε αλλαγές γραμμών, για παράδειγμα) που δεν έχουν πραγματική τιμή Unicode, και η CharacterMapError[i] σηματοδοτεί ότι το PDFium δεν μπόρεσε να αντιστοιχίσει τον γλύφο σε ένα σημείο κώδικα, κάτι που συμβαίνει με κωδικοποιήσεις γραμματοσειρών που στερούνται πίνακα ToUnicode

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Το αποτέλεσμα είναι μια επίπεδη συμβολοσειρά από σημεία κώδικα Unicode με τη σειρά που τα απαριθμεί το PDFium, η οποία είναι η σειρά με την οποία εμφανίζονται στη ροή περιεχομένου, όχι απαραίτητα με τη σειρά ανάγνωσης από αριστερά προς τα δεξιά. Για τα περισσότερα έγγραφα λατινικής γραφής που παράγονται από τυπικά εργαλεία γραφείου, αυτό είναι μια χαρά. Για σαρωμένα PDFs που υποβλήθηκαν σε OCR με ασυνήθιστες αλληλουχίες γλύφων, ή για κείμενο από δεξιά προς τα αριστερά, η σειρά μπορεί να είναι λάθος. Τότε είναι που το ReadablePageContent γίνεται πιο χρήσιμο

Δομημένη εξαγωγή με το ReadablePageContent

Το ReadablePageContent ανεβαίνει ένα επίπεδο: επιστρέφει μια εγγραφή TPdfReadableContent της οποίας ο πίνακας Fragments μεταφέρει επισημασμένα αποσπάσματα περιεχομένου, το καθένα με ένα Kind που προσδιορίζει παραγράφους, επικεφαλίδες, στοιχεία λίστας, κελιά πίνακα κ.λπ. Όταν το PDF φέρει ένα δέντρο δομής (ελέγξτε το Pdf.IsTagged), η πηγή είναι η rosStructure και η σειρά ανάγνωσης είναι αυθεντική. Για έγγραφα χωρίς ετικέτες, το PDFium επιστρέφει στη rosHeuristic, η οποία ομαδοποιεί τους χαρακτήρες με βάση τα πλαίσια οριοθέτησής τους (bounding boxes) σε εύλογες μονάδες ανάγνωσης, αλλά δεν μπορεί να εγγυηθεί την ακρίβεια

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Εάν το Content.Source = rosHeuristic και το αποτέλεσμα φαίνεται μπερδεμένο, το επίπεδο κειμένου του εγγράφου πιθανότατα δεν γράφτηκε με γνώμονα τη σειρά ανάγνωσης. Σε αυτό το σημείο, η μόνη αξιόπιστη διόρθωση είναι η εκ νέου εξαγωγή από την εφαρμογή προέλευσης με κατάλληλη σήμανση ετικετών, ή η εκτέλεση ενός βήματος μετα-επεξεργασίας που ταξινομεί τις προελεύσεις των χαρακτήρων κατά Y και μετά κατά X

Τι σας δίνουν τα CharacterOrigin και CharacterRectangle

Και οι δύο ιδιότητες επιστρέφουν τη θέση ενός χαρακτήρα στο χώρο της σελίδας (σημεία, προέλευση στην κάτω αριστερή γωνία, το Y αυξάνεται προς τα πάνω). Το CharacterOrigin[i] είναι το σημείο αγκύρωσης της γραμμής βάσης του γλύφου· το CharacterRectangle[i] είναι το πλήρες πλαίσιο οριοθέτησης. Αυτά είναι τα δομικά στοιχεία για οτιδήποτε πέρα από το απλό κείμενο: ανίχνευση ορίων στηλών, ομαδοποίηση χαρακτήρων σε γραμμές με σύγκριση συντεταγμένων Y εντός ανοχής, ή δημιουργία χάρτη hit-test για επιλογή κειμένου σε ένα πρόγραμμα προβολής. Εάν πρέπει να βρείτε ποιος χαρακτήρας βρίσκεται κάτω από ένα κλικ του ποντικιού, η CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) εκτελεί αυτήν την αναζήτηση απευθείας χωρίς να χρειάζεται να διατρέξετε ορθογώνια

Τοποθέτηση του DLL στη θέση του

Το PDFium Component αναθέτει όλη την ανάλυση PDF σε ένα εγγενές DLL, είτε το pdfium32.dll είτε το pdfium64.dll ανάλογα με την πλατφόρμα-στόχο σας. Το στοιχείο συνοδεύεται από ένα σενάριο CopyDlls.bat script που αντιγράφει το σωστό αρχείο στον κατάλογο συστήματος των Windows. Η εκτέλεσή του ως Διαχειριστής μία φορά στο μηχάνημα ανάπτυξης είναι αρκετή· για την ανάπτυξη (deployment) αντιγράφετε το DLL δίπλα στο εκτελέσιμο της εφαρμογής. Οι παραλλαγές με δυνατότητα V8 (pdfium32v8.dll, pdfium64v8.dll) είναι σημαντικά μεγαλύτερες και απαραίτητες μόνο εάν τα PDFs σας περιέχουν JavaScript που πρέπει να εκτελεστεί. Για καθαρή εξαγωγή κειμένου, η τυπική έκδοση είναι η σωστή επιλογή

Εάν το DLL απουσιάζει κατά τον χρόνο εκτέλεσης, το Active := True θα αποτύχει σιωπηλά όπως ακριβώς συμβαίνει για ένα αρχείο που λείπει, επειδή το στοιχείο συλλαμβάνει το σφάλμα φόρτωσης εσωτερικά. Πάντα να δοκιμάζετε σε ένα καθαρό μηχάνημα πριν από την αποστολή

Χρήση του FontSize[] παράλληλα με το Character[] για ανάλυση διάταξης

Πέρα από το απλό κείμενο, το API επιπέδου χαρακτήρα εκθέτει το FontSize[i], το οποίο επιστρέφει το μέγεθος σημείου (point size) κάθε γλύφου όπως αποδίδεται. Σε συνδυασμό με τα CharacterOrigin[i] και CharacterRectangle[i], αυτό σας επιτρέπει να διακρίνετε το κείμενο σώματος από τις κεφαλίδες χωρίς να βασίζεστε στο δέντρο δομής. Μια σειρά χαρακτήρων όπου το μέγεθος της γραμματοσειράς υπερβαίνει ένα όριο είναι σχεδόν βέβαιο ότι είναι κεφαλίδα σε ένα έγγραφο χωρίς ετικέτες. Η ίδια τεχνική εφαρμόζεται για την ανίχνευση λεζαντών (μικρό κείμενο κάτω από το πλαίσιο οριοθέτησης μιας εικόνας) ή υποσημειώσεων (μικρό κείμενο κοντά στο κάτω μέρος της σελίδας). Τίποτα από αυτά δεν απαιτεί απόδοση (rendering)· και οι τρεις ιδιότητες διαβάζονται απευθείας από το επίπεδο κειμένου που δημιουργεί το PDFium κατά τη διάρκεια του Active := True

Μια λεπτομέρεια: το FontSize[i] αντικατοπτρίζει το μέγεθος μετά την εφαρμογή του CTM (μήτρα τρέχοντος μετασχηματισμού) της σελίδας, οπότε ένα έγγραφο όπου ο δημιουργός άλλαξε την κλίμακα ολόκληρης της σελίδας θα αναφέρει μεγέθη αναλογικά προσαρμοσμένα. Εάν συγκρίνετε μεγέθη μεταξύ σελίδων με διαφορετικές διαστάσεις, κανονικοποιήστε τα έναντι του ύψους MediaBox κάθε σελίδας πριν λάβετε αποφάσεις ορίου

Εγγραφή του αποτελέσματος σε αρχείο

Το TStringList του Delphi χειρίζεται καθαρά την έξοδο UTF-8 από την έκδοση XE. Ορίστε το WriteBOM := False εάν χρειάζεστε ένα αρχείο χωρίς BOM (πολλοί κατάντη καταναλωτές κολλάνε σε ένα αρχικό BOM):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

Για πολύ μεγάλα έγγραφα όπου η μνήμη αποτελεί πρόβλημα, γράψτε απευθείας σε ένα TStreamWriter με TEncoding.UTF8 μέσα στο βρόχο σελίδας, αντί να συσσωρεύετε τα πάντα σε μια λίστα πρώτα

Τα APIs Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent και CharacterIndexAtPos που εμφανίζονται εδώ αποτελούν μέρος του PDFium Component για Delphi και C++Builder