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

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

Το PDF αποθηκεύει εικόνες ως αντικείμενα πρώτης κατηγορίας μέσα στις ροές περιεχομένου του. Όταν μια σελίδα αναφέρεται σε μια φωτογραφία, μια σάρωση ή ένα διάγραμμα, τα δεδομένα pixel ζουν σε ένα λεξικό XObject μαζί με τη γεωμετρία της σελίδας. Το PDFium Component το εμφανίζει αυτό μέσω δύο ιδιοτήτων στο TPdf: την BitmapCount, η οποία επιστρέφει πόσα ενσωματωμένα bitmap υπάρχουν στην τρέχουσα σελίδα, και την Bitmap[Index], η οποία αποκωδικοποιεί ένα από αυτά σε ένα TBitmap που σας ανήκει και πρέπει να απελευθερώσετε. Αυτό είναι ολόκληρο το μοντέλο εξαγωγής. Ο βρόχος είναι τέσσερις γραμμές· αυτό που απαιτεί κρίση είναι η γύρω υδραυλική (plumbing)

Άνοιγμα του εγγράφου

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

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

Τα αρχεία που προστατεύονται με κωδικό πρόσβασης ακολουθούν το ίδιο μοτίβο: αναθέστε το Pdf.Password πριν ορίσετε το Active := True. Αν ο κωδικός πρόσβασης είναι λάθος, το Active παραμένει False και δεν έχετε καμία εξαίρεση για να πιάσετε. Σε ένα εργαλείο δέσμης (batch tool) που επεξεργάζεται εκατοντάδες αρχεία, αυτή η σιωπηλή συμπεριφορά είναι στην πραγματικότητα χρήσιμη: συσσωρεύετε τις αποτυχίες σε μια λίστα αντί να ξετυλίγετε τη στοίβα κλήσεων για καθεμία

Επανάληψη σελίδων και άντληση bitmap

Το BitmapCount είναι ανά σελίδα, επομένως ρυθμίζετε το Pdf.PageNumber πριν το διαβάσετε. Οι αριθμοί σελίδων βασίζονται στο 1· η προεπιλογή είναι 0, που σημαίνει ότι δεν έχει φορτωθεί καμία σελίδα. Η ιδιότητα Bitmap[Index] βασίζεται στο 0 και επιστρέφει ένα TBitmap που ανήκει στον καλούντα. Πρέπει να το απελευθερώσετε. Αν αμελήσετε την απελευθέρωση (free) μέσα σε έναν μεγάλο βρόχο πάνω από ένα μεγάλο έγγραφο η μνήμη σκαρφαλώνει γρήγορα, επειδή κάθε bitmap μπορεί να είναι αρκετά megabyte ακατέργαστων δεδομένων pixel πριν από οποιαδήποτε συμπίεση

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

Ο έλεγχος (guard) Assigned έχει σημασία. Ένας μικρός αριθμός γεννητριών PDF γράφει XObjects εικόνας με μηδενικές διαστάσεις pixel ή άλλως κακοσχηματισμένα δεδομένα· σε αυτές τις περιπτώσεις το στοιχείο επιστρέφει nil αντί για ένα κενό bitmap. Η αντιμετώπιση μιας επιστροφής nil ως σφάλματος και η διακοπή της εξαγωγής είναι το λάθος αντανακλαστικό: παραλείψτε το, καταγράψτε τη σελίδα και το ευρετήριο αν χρειάζεστε το ίχνος ελέγχου και συνεχίστε. Το υπόλοιπο της σελίδας μπορεί ακόμα να αποδώσει έγκυρες εικόνες

Παρατηρήστε ότι ο εξωτερικός βρόχος ρυθμίζει το Pdf.PageNumber σε κάθε επανάληψη. Αυτή η ανάθεση είναι που φορτώνει τη σελίδα στην εσωτερική κατάσταση του στοιχείου και δίνει νόημα στο BitmapCount. Αν την παραλείψετε θα διαβάζετε την καταμέτρηση της ίδιας σελίδας επανειλημμένα. Το μοτίβο μοιάζει περιττό όταν το γράφετε, αλλά έτσι είναι σχεδιασμένο το API: η σελίδα είναι ένας κέρσορας, όχι μια συλλογή

Επιλογή μορφής εξόδου

Το BMP είναι χωρίς απώλειες και πάντα διαθέσιμο χωρίς πρόσθετες μονάδες, γεγονός που το καθιστά μια καλή προεπιλογή όταν δεν γνωρίζετε ακόμη τι περιέχει η εικόνα. Όταν το μέγεθος του αρχείου έχει σημασία, η μορφή pixel του TBitmap που επιστρέφεται σας λέει ποιος κωδικοποιητής είναι κατάλληλος. Ένα bitmap 32-bit φέρει ένα κανάλι alpha· το PNG το διατηρεί αυτό χωρίς απώλειες. Μια μεγάλη εικόνα 24-bit με συνεχή τόνο είναι υποψήφια για JPEG. Μικρότερες εικόνες ή αυτές που σχεδιάζονται με περιορισμένη παλέτα είναι γενικά καλύτερο να αφεθούν ως BMP παρά να περάσουν από JPEG, το οποίο προσθέτει τεχνουργήματα (artifacts) μπλοκαρίσματος σε χαμηλές ρυθμίσεις ποιότητας και εξοικονομεί ελάχιστα σε υψηλές

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: lossless, no extra units
  end;
end;

Στην πράξη, η επιλογή μορφής καθοδηγείται από το Bmp.PixelFormat και τις διαστάσεις. Αν PixelFormat = pf32bit χρειάζεστε μια μορφή που φέρει alpha· το PNG είναι η προφανής επιλογή, αν και απαιτεί τη μονάδα PNGImage σε παλαιότερες εκδόσεις του Delphi. Για εικόνες 24-bit με πλάτος μεγαλύτερο από περίπου 300 pixel, το JPEG σε ποιότητα 85 δίνει μείωση μεγέθους τρία προς ένα σε σχέση με το BMP χωρίς αισθητή απώλεια στο μεγαλύτερο μέρος του φωτογραφικού περιεχομένου. Κάτω από αυτό το όριο, το BMP είναι συγκρίσιμο σε μέγεθος και αποφεύγει εντελώς οποιαδήποτε απόφαση ποιότητας

Τι μετράει και τι δεν μετράει το BitmapCount

Το PDF κάνει διάκριση μεταξύ XObjects εικόνας και διανυσματικών γραφικών (vector graphics) που σχεδιάζονται με τελεστές διαδρομής (path). Μια σελίδα που φαίνεται οπτικά πολύπλοκη μπορεί να επιστρέψει ένα BitmapCount μηδέν αν κάθε στοιχείο είναι διάνυσμα. Οι σαρωμένες σελίδες σχεδόν πάντα επιστρέφουν ακριβώς ένα: ο σαρωτής γράφει ολόκληρη τη σάρωση ως ένα ενιαίο XObject εικόνας πλήρους σελίδας σε όποια ανάλυση είχε ρυθμιστεί ο σαρωτής. Οι σελίδες που αναμειγνύουν στοιχειοθετημένο κείμενο με ενσωματωμένες φωτογραφίες επιστρέφουν μία καταχώρηση ανά φωτογραφία. Οι διακοσμητικές γραμμές, τα σκιασμένα φόντα και τα περιγράμματα πινάκων συνήθως δεν εμφανίζονται καθόλου στην καταμέτρηση bitmap

Η καταμέτρηση δεν περιλαμβάνει επίσης ενσωματωμένες (inline) εικόνες, μια σπάνια χρησιμοποιούμενη δομή PDF όπου τα δεδομένα εικόνας είναι ενσωματωμένα απευθείας στη ροή περιεχομένου της σελίδας αντί ως ένα ονομασμένο XObject. Αυτά βρίσκονται εκτός των όσων εμφανίζει αυτό το API· είναι αρκετά ασυνήθιστα σε πραγματικά έγγραφα, ώστε τα περισσότερα εργαλεία εξαγωγής απλά να μην τα χειρίζονται

Μια λεπτομέρεια που αξίζει να έχετε κατά νου: το BitmapCount που διαβάζετε αφορά την τρέχουσα σελίδα από την τελευταία ανάθεση του PageNumber. Αν ο κώδικάς σας διακλαδίζεται ή καλεί οποιαδήποτε συνάρτηση που αλλάζει το PageNumber μεταξύ της καταμέτρησης και της άντλησης (fetching), μπορεί να διαβάσετε λιγότερες εικόνες από αυτές για τις οποίες εκχωρήσατε χώρο, ή να βγείτε εκτός ορίων του ευρετηρίου (index past the end). Κρατήστε την ανάγνωση της καταμέτρησης και τον βρόχο Bitmap[] στην ίδια σελίδα χωρίς να αγγίξετε το PageNumber στο ενδιάμεσο

Χρήση του TPdfView σε μια εφαρμογή φόρμας

Το στοιχείο TPdfView εκθέτει τις ίδιες ιδιότητες BitmapCount και Bitmap[], αλλά η σελίδα από την οποία διαβάζει είναι η τρέχουσα εμφανιζόμενη σελίδα της προβολής, όχι το TPdf.PageNumber. Οι δύο δείκτες (pointers) σελίδων είναι ανεξάρτητοι· ο ορισμός του ενός δεν μετακινεί τον άλλο. Σε μια εφαρμογή φόρμας VCL με ένα ζωντανό πρόγραμμα προβολής, μπορείτε να καλέσετε Pdf.PageNumber := N για να οδηγήσετε την εξαγωγή μέσω του TPdf ενώ το πρόγραμμα προβολής παραμένει σε ό,τι κύλησε (scrolled) τελευταία ο χρήστης. Αυτός ο διαχωρισμός είναι σκόπιμος και διατηρεί καθαρή την κατάσταση εμφάνισης του προγράμματος προβολής ενώ εκτελείται μια εξαγωγή στο παρασκήνιο

Μνήμη και απόδοση σε εργασίες δέσμης (batch jobs)

Σε ένα μεγάλο αρχείο ο προϋπολογισμός μνήμης είναι το κύριο πράγμα που πρέπει να προσέχετε. Κάθε κλήση Bitmap[] εκχωρεί ένα νέο TBitmap στον σωρό (heap), και σε μια σαρωμένη σελίδα 300 DPI αυτό είναι εύκολα 25 MB ακατέργαστων δεδομένων pixel πριν από οποιαδήποτε κωδικοποίηση. Αν επεξεργαστείτε σελίδες σε έναν στενό βρόχο (tight loop) χωρίς απελευθέρωση (freeing) μεταξύ των επαναλήψεων, το σύνολο εργασίας μεγαλώνει γραμμικά με τον αριθμό των εικόνων. Το σωστό σχήμα είναι πάντα: φέρτε ένα bitmap, κάντε αυτό που χρειάζεστε, απελευθερώστε το, φέρτε το επόμενο. Αν χρειάζεται να κρατήσετε αναφορές σε πολλά bitmap ταυτόχρονα για ένα βήμα σύγκρισης, μετρήστε τα πρώτα με το BitmapCount και κατανείμετε το δοχείο σας ανάλογα, στη συνέχεια απελευθερώστε το καθένα αμέσως μόλις τελειώσετε με αυτό, αντί να το αναβάλλετε για εκκαθάριση στο τέλος του εγγράφου. Σε ένα έγγραφο με 500 σαρωμένες σελίδες αυτή η διάκριση μπορεί να σημαίνει τη διαφορά μεταξύ 25 MB και 12 GB μέγιστου RSS (peak RSS)

Οι ιδιότητες BitmapCount και Bitmap[] που εμφανίζονται εδώ αποτελούν μέρος του PDFium Component για Delphi και C++Builder