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

Εξαγωγή εικόνων από ένα φορτωμένο PDF στο Delphi: HotPDF

Έχετε ένα PDF στον δίσκο, ένας πελάτης το σάρωσε από μια στοίβα τιμολογίων, και η δουλειά σας είναι να ξαναβγάλετε τις εικόνες των σελίδων ως bitmaps για ένα πέρασμα OCR. Φορτώνετε το αρχείο, βρίσκετε τα image XObjects, και τότε ανακαλύπτετε το σημείο για το οποίο κανείς δεν σας προειδοποιεί: τα bytes σε αυτές τις ροές δεν είναι pixels. Είναι ένα JPEG codestream, ή ένα blob JPEG 2000 συμπιεσμένο με wavelet, ή μια ροή φαξ Group 4, ή ένα indexed raster πίσω από μια παλέτα πίσω από ένα φίλτρο Flate. Το image object ξέρει το πλάτος και το ύψος του, αλλά τα πραγματικά δείγματα είναι σφραγισμένα μέσα στο φίλτρο που διάλεξε ο παραγωγός. Η απόκτηση ενός χρήσιμου TBitmap σημαίνει να αναιρέσετε αυτό το φίλτρο, και το PDF σας δίνει περίπου οκτώ διαφορετικούς τρόπους με τους οποίους μπορούν να είναι σφραγισμένα τα bytes

Αυτό είναι το κενό που ExtractLoadedImage καλύπτει στο HotPDF, το εγγενές στοιχείο PDF VCL για Delphi και C++Builder. Καταγράφει τα image XObjects σε ένα έγγραφο που φορτώσατε, αναφέρει τι είναι το καθένα και αποκωδικοποιεί όσα μπορεί πίσω σε bitmap 24 bit. Το ενδιαφέρον δεν είναι η επιφάνεια του API, που είναι τρεις μέθοδοι. Είναι γιατί πρέπει εξαρχής να υπάρχει ξεχωριστή διαδρομή αποκωδικοποίησης και τι μπορεί και τι δεν μπορεί να μετατρέψει ξανά σε pixels

Γιατί οι εικόνες που έχουν φορτωθεί δεν είναι ήδη αποκωδικοποιημένες

Ο φορτωτής του HotPDF είναι χτισμένος γύρω από την πιστότητα passthrough. Όταν καλείτε LoadFromFile, οι ροές εικόνας διατηρούνται ακριβώς όπως εμφανίζονται στο αρχείο προέλευσης: το αρχικό φίλτρο, τα αρχικά συμπιεσμένα bytes, το αρχικό λεξικό. Αυτό είναι σκόπιμο. Ολόκληρος ο σκοπός της φόρτωσης ενός εγγράφου είναι συνήθως να αντιγράψετε σελίδες, να συγχωνεύσετε αρχεία, να τα σφραγίσετε, να αλλάξετε τα δικαιώματά τους και να τα γράψετε ξανά, και για όλα αυτά το φθηνότερο και ασφαλέστερο είναι να μείνει κάθε ροή εικόνας ανέπαφη. Η αποκωδικοποίηση κάθε εικόνας σε raster κατά τη φόρτωση θα έκαιγε μνήμη και CPU για δουλειά που οι περισσότεροι καλούντες δεν χρειάζονται ποτέ, και η εκ νέου κωδικοποίηση κατά την αποθήκευση θα υποβάθμιζε εικόνες που έπρεπε να είχαν αντιγραφεί αυτολεξεί

Η συνέπεια είναι ότι το φορτωμένο γράφημα αντικειμένων δεν περιέχει pixels. Ένα image XObject του οποίου το /Filter είναι /DCTDecode περιέχει bytes JPEG· το HotPDF δεν έτρεξε ποτέ πάνω του έναν αποκωδικοποιητή JPEG, επειδή τίποτα στη διαδρομή αντιγραφής και επανεγγραφής δεν το χρειαζόταν. Έτσι, όταν πραγματικά θέλετε pixels, το API εξαγωγής πρέπει να κάνει την αποκωδικοποίηση μόνο του, από την αρχή, για όποιο φίλτρο χρησιμοποιεί τυχαία η συγκεκριμένη εικόνα. Αυτός είναι και ο λόγος που οι codecs της πλευράς κωδικοποίησης είναι ανεξάρτητοι από τον loader: το άρθρο για την προσθήκη εικόνων JPEG 2000 σε PDF στο Delphi περιγράφει πώς η μηχανή JPX συνδέεται με την πλευρά δημιουργίας, και εκείνη η μηχανή απλώς δεν είχε συνδεθεί με τη διαδρομή ανάγνωσης μέχρι να τη χρειαστεί το API εξαγωγής

Το API των τριών μεθόδων

Η επιφάνεια είναι μικρή. GetLoadedImageCount επιστρέφει πόσα image XObjects περιέχει το φορτωμένο έγγραφο. GetLoadedImageInfo γεμίζει μια εγγραφή περιγραφής για ένα από αυτά με βάση το index. ExtractLoadedImage επιστρέφει το αποκωδικοποιημένο bitmap, ή nil όταν δεν μπορεί να αποκωδικοποιήσει αυτή την εικόνα. Η απαρίθμηση βασίζεται σε index και είναι σταθερή για ένα συγκεκριμένο load: εσωτερικά διατρέχει τον πίνακα έμμεσων αντικειμένων και συγκεντρώνει κάθε ροή της οποίας το /Subtype επιλύεται σε /Image, οπότε το index που περνάτε στο GetLoadedImageInfo είναι το ίδιο index που περνάτε στο ExtractLoadedImage

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I, Count: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-invoices.pdf', '') <= 0 then
      Exit;
    Count := Pdf.GetLoadedImageCount;
    for I := 0 to Count - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if not Info.Decodable then
        Continue;                       // filter or colour space not supported
      Bmp := Pdf.ExtractLoadedImage(I);
      if Bmp <> nil then
      try
        Bmp.SaveToFile(Format('img_%d.bmp', [I]));
      finally
        Bmp.Free;                       // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Δύο λεπτομέρειες του συμβολαίου έχουν σημασία εδώ. Πρώτον, το επιστρεφόμενο TBitmap είναι δικό σας να το ελευθερώσετε· το έγγραφο δεν το αποθηκεύει στην cache ούτε το κατέχει. Δεύτερον, ελέγξτε το Decodable πριν καλέσετε, και ελέγξτε το αποτέλεσμα σε σχέση με το nil μετά. Η μέθοδος δεν ρίχνει εξαίρεση σε μη υποστηριζόμενο φίλτρο, επιστρέφει nil, και μια σιωπηλή nil σε ένα batch loop είναι ακριβώς το είδος του πράγματος που καταπίνει μια σελίδα από εργασία χιλίων σελίδων χωρίς να το προσέξει κανείς

Διαβάζοντας την περιγραφή πριν αποκωδικοποιήσετε

σας λέει τι είναι μια εικόνα χωρίς να δεσμευτείτε σε πλήρη αποκωδικοποίηση. Τα πεδία της προέρχονται κατευθείαν από το image dictionary: THPDFLoadedImageInfo και Width σε samples, Height, BitsPerComponent και ColorComponents που περιγράφουν την ερμηνεία μετά την αποκωδικοποίηση (1 για gray, 3 για RGB, 4 για CMYK), ColorSpace ως η ονομασμένη συμπίεση, Filter για μάσκες στένσιλ, IsImageMask για το υποκείμενο έμμεσο αντικείμενο, και ObjectNumber.DecodableΑυτή η τελευταία σημαία είναι η πιο ειλικρινής

είναι Decodable μόνο όταν η τρέχουσα build μπορεί πραγματικά να μετατρέψει αυτόν τον συγκεκριμένο συνδυασμό φίλτρου και χρωματικού χώρου σε bitmap. Κωδικοποιεί τον πραγματικό πίνακα υποστήριξης, όχι μια ευχή: μια εικόνα της οποίας το True που η τρέχουσα build δεν καταλαβαίνει αναφέρει Filter, και μπορείτε να κάνετε branching πάνω σε αυτό για να καταγράψετε, να παραλείψετε ή να επιστρέψετε στην εξαγωγή του ακατέργαστου ρεύματος μόνοι σας. Αντιμετωπίστε το ως προϋπόθεση, όχι ως υπόδειξη.Decodable = FalseΜια λεπτομέρεια υλοποίησης μπερδεύει όσους χτίζουν εγγραφές περιγραφικών στοιχείων με το χέρι

// Triage every image before committing to a decode.
var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  I: Integer;
begin
  // ... Pdf loaded ...
  for I := 0 to Pdf.GetLoadedImageCount - 1 do
  begin
    if not Pdf.GetLoadedImageInfo(I, Info) then
      Continue;
    if Info.Decodable then
      // ExtractLoadedImage(I) will return a TBitmap
    else
      // unsupported filter/colour space: log the object and skip
      Writeln(Format('Image %d obj %d: %dx%d %s/%s not decodable',
        [I, Info.ObjectNumber, Info.Width, Info.Height,
         String(Info.Filter), String(Info.ColorSpace)]));
  end;
end;

περιέχει δύο THPDFLoadedImageInfo πεδία, AnsiString και Filter. Αυτοί είναι διαχειριζόμενοι τύποι με καταμέτρηση αναφορών, οπότε το ένστικτο να μηδενίσετε μια εγγραφή με ColorSpace είναι λάθος εδώ: αντικαθιστά την αναφορά της συμβολοσειράς χωρίς να τη μειώνει, κάτι που προκαλεί διαρροή ή φθορά. Το HotPDF αρχικοποιεί την εγγραφή πεδίο προς πεδίο ακριβώς για αυτόν τον λόγο, και αν ποτέ αντιγράψετε αυτό το μοτίβο στον δικό σας κώδικα, κάντε το ίδιο.FillChar(Info, SizeOf(Info), 0)Ένας διανομέας, οκτώ διαδρομές φίλτρων

Ο λόγος που αυτή η δυνατότητα χρειάστηκε μια σειρά εκδόσεων αντί για μία είναι ότι το PDF δεν έχει μορφή εικόνας. Έχει φίλτρα, και η §8.9.5 του ISO 32000-1 επιτρέπει σε ένα image XObject να ονομάζει οποιοδήποτε από αυτά στο

, με την ερμηνεία των δειγμάτων να ορίζεται ξεχωριστά από το /Filter, /ColorSpace και έναν προαιρετικό /BitsPerComponent πίνακα. /Decode διαβάζει το όνομα του φίλτρου και δρομολογεί σε έναν ειδικό αποκωδικοποιητή για κάθε περίπτωση. Το υποστηριζόμενο σύνολο, που χτίστηκε από την v2.229 έως την v2.231, καλύπτει πλέον οκτώ ξεχωριστές διαδρομές.ExtractLoadedImageΑκατέργαστα rasters (FlateDecode, LZWDecode ή χωρίς φίλτρο)

  • σε 8-bit DeviceRGB ή DeviceGray. Τα bytes αποσυμπιέζονται σε ένα packed raster, και ο μόνος μετασχηματισμός είναι η εναλλαγή καναλιών, που καλύπτεται παρακάτω.DCTDecode (JPEG)
  • . Το codestream παραδίδεται στο , το οποίο επιλύει τη γεωμετρία και το χρώμα, και το αποτέλεσμα αποδίδεται σε ένα bitmap 24 bit.TJPEGImageJPXDecode (JPEG 2000)
  • . Αποκωδικοποιείται μέσω του backend OpenJPEG, της ίδιας μηχανής που περιγράφεται στο άρθρο για το JPEG 2000, με τα στοιχεία υψηλού βάθους bit να επαναδειγματοληπτούνται προς τα κάτω στα 8 bits.Δεικτοδοτημένο χρώμα
  • . Η παλέτα διαβάζεται από τον πίνακα και κάθε δείγμα επεκτείνεται μέσω του πίνακα αναζήτησης σε αληθινό χρώμα.[/Indexed base hival lookup]DeviceCMYK
  • . Τα δείγματα τεσσάρων καναλιών μετατρέπονται σε RGB με τον τυπικό τύπο μελάνι πάνω σε λευκό.DeviceGray και Indexed κάτω από 8 bit
  • σε 1, 2 ή 4 bits ανά συνιστώσα, αποσυμπιέζονται δείγμα προς δείγμα και κλιμακώνονται στο εύρος 0–255.CCITTFaxDecode
  • , τα φίλτρα φαξ Group 3 και Group 4, αποκωδικοποιούνται από ειδικό backend T.4/T.6.JBIG2Decode
  • , το φίλτρο bilevel υψηλής συμπίεσης, αποκωδικοποιείται μέσω του καταχωρισμένου backend JBIG2 που το Άρθρο για native JBIG2 compression καλύπτει από την πλευρά της κωδικοποίησης.Όλα καταλήγουν στο ίδιο σημείο: ένα bitmap BGR 24 bit, επειδή αυτό είναι ό,τι ένα VCL

αποθηκεύει εγγενώς και ό,τι περιμένει κάθε downstream consumer.TBitmapΟι μετασχηματισμοί που αλλάζουν σιωπηλά τα pixels

Δύο από αυτές τις διαδρομές περιλαμβάνουν έναν μετασχηματισμό που είναι εύκολο να γίνει λίγο λάθος, και αξίζει να τον κατανοήσετε ακόμη κι αν δεν αγγίξετε ποτέ ο ίδιος τον αποκωδικοποιητή. Ο πρώτος είναι η εναλλαγή της σειράς χρωμάτων. Ένα PDF DeviceRGB raster αποθηκεύει τα δείγματα σε σειρά κόκκινο-πράσινο-μπλε, με την επάνω γραμμή πρώτη. Ένα VCL 24-bit scanline τα αποθηκεύει σε σειρά μπλε-πράσινο-κόκκινο. Άρα η αποκωδικοποίηση ενός απλού RGB image δεν είναι memcpy· κάθε πρώτο και τρίτο byte του pixel ανταλλάσσονται καθώς μπαίνει στο scanline. Αν το κάνετε ανάποδα, τα κόκκινα και τα μπλε αλλάζουν θέση, κάτι που φαίνεται μια χαρά σε μια grayscale test image και καταστροφικά λάθος σε μια colour one. Η σειρά των γραμμών, όσο κι αν αξίζει να το πούμε, περνάει κατευθείαν: τα top-down rasters του PDF ευθυγραμμίζονται με το

ως την επάνω οπτική γραμμή, οπότε δεν χρειάζεται κάθετη αναστροφή.ScanLine[0]Ο δεύτερος είναι το CMYK. Οι εικόνες PDF DeviceCMYK μεταφέρουν τέσσερα μελάνια, και η μετατροπή σε RGB είναι υπολογισμός ανά κανάλι, όχι αναζήτηση: κάθε κανάλι εξόδου είναι

. Αυτή είναι μια προσεγγιστική μετατροπή συσκευής, όχι μια μετατροπή με διαχείριση χρώματος μέσω ICC profile, οπότε το αποτέλεσμα είναι αρκετά σωστό για προβολή και επαναραστεροποίηση, αλλά δεν είναι η σωστή διαδρομή αν χρειάζεστε χρωματική ακρίβεια εκτύπωσης. Αν η ροή εργασίας σας απαιτεί πιστότητα, αντιμετωπίστε το εξαγόμενο bitmap ως προεπισκόπηση και κρατήστε το αρχικό ρεύμα CMYK για τη χρωματικά διαχειριζόμενη αλυσίδα.(255 - ink) * (255 - K) / 255Η διαδρομή Indexed κρύβει και τη δική της παγίδα ανάλυσης. Η παλέτα σε έναν

χρωματικό χώρο μπορεί να αποθηκευτεί ως literal string ή ως hexadecimal string, και το HotPDF αποθηκεύει την τιμή ενός hex string ως το hex /Indexedtext, όχι ως τα decoded bytes. Άρα όταν η παλέτα είναι hex string, ο lookup table πρέπει πρώτα να περάσει από decode από hex σε bytes· ένα literal string είναι ήδη raw bytes. Αν χάσετε αυτό το κλαδί, μια indexed image τεσσάρων χρωμάτων βγαίνει σκουπίδι, επειδή κάθε εγγραφή της παλέτας διαβάζεται από λάθος όριο byte.Αλυσίδες φίλτρων: το τελευταίο φίλτρο είναι η ίδια η μορφή της εικόνας

Ένα μόνο

όνομα είναι η εύκολη περίπτωση. Το PDF επιτρέπει επίσης μια /Filterαλυσίδα από φίλτρα, όπου το ρεύμα έχει περάσει διαδοχικά από περισσότερα, καταχωρισμένα με τη σειρά σε έναν πίνακα όπως /Filter ή [/ASCII85Decode /FlateDecode] (ISO 32000-1 §7.4). Η σημασιολογία είναι ακριβής: τα φίλτρα εφαρμόζονται από αριστερά προς τα δεξιά στην κωδικοποίηση, άρα στην αποκωδικοποίηση τα αναιρείτε από δεξιά προς τα αριστερά, και το [/ASCIIHexDecode /DCTDecode]τελευταίο φίλτρο στον πίνακα είναι αυτό που ορίζει πραγματικά τη μορφή της εικόνας. Τα πρώτα φίλτρα είναι απλώς transport encodings τυλιγμένα γύρω του.Ο extractor το χειρίζεται αυτό αφαιρώντας στρώσεις. Πριν τρέξει οποιοσδήποτε image decoder, εφαρμόζεται κάθε φίλτρο στην αλυσίδα εκτός από το τελευταίο για να παραχθεί η είσοδος που περιμένει το τελικό φίλτρο, και μόνο τότε γίνεται η δρομολόγηση σε εκείνο το τελευταίο φίλτρο. Έτσι

πρώτα αφαιρεί το ASCII85 από το stream, και μετά δρομολογεί το αποτέλεσμα στη διαδρομή JPEG· [/ASCII85Decode /DCTDecode] τυλιγμένο γύρω από ένα raw raster το αποσυμπιέζει και μετά τρέχει τη διαδρομή raster. Αυτό είναι που κρατά τους οκτώ decoders απλούς. Κανένας τους δεν χρειάζεται να ξέρει για ASCII85 ή hex transport wrappers, επειδή τη στιγμή που ένας decoder βλέπει τα bytes, τα wrappers έχουν ήδη φύγει. Σημαίνει επίσης ότι μια αλυσίδα της οποίας το τελικό φίλτρο δεν υποστηρίζεται εξακολουθεί να αποτυγχάνει καθαρά στο στάδιο της δρομολόγησης και όχι στη μέση.[/FlateDecode]Πού σταματά η εξαγωγή, και τι να κάνετε τότε

Να είστε ειλικρινείς με τον εαυτό σας για τα όρια. Μια εικόνα της οποίας το τελικό φίλτρο είναι εκτός του υποστηριζόμενου συνόλου επιστρέφει

, και το ίδιο κάνει και μια εικόνα της οποίας ο χρωματικός χώρος δεν μπορεί να ερμηνευτεί από το build. Τα soft masks και το alpha δεν ανακατασκευάζονται στο bitmap· παίρνετε τη βάση της εικόνας, όχι ένα composited αποτέλεσμα. Τα bit depths πάνω από 8 από JPEG 2000 γίνεται resample προς τα κάτω, κάτι που είναι σκόπιμα lossy και η λάθος κίνηση αν κάνετε επανααρχειοθέτηση αντί για προβολή. Και ένα image mask, ένα one-bit stencil χωρίς δικό του χρώμα, περιγράφεται από το descriptor αλλά είναι κάτι διαφορετικό από μια pictorial image· αν το αποκωδικοποιήσετε περιμένοντας φωτογραφία, θα εκπλαγείτε.nilΌταν η εξαγωγή δεν αρκεί, το raw stream εξακολουθεί να βρίσκεται εκεί μέσα στο loaded object graph, φίλτρο και όλα, και μπορείτε να το τραβήξετε byte προς byte και να το δώσετε σε έναν εξειδικευμένο codec δικό σας. Αυτό είναι το fallback που διατηρεί σκόπιμα το pass-through design: τα αρχικά bytes δεν πετιούνται ποτέ, οπότε στη χειρότερη περίπτωση τα αποκωδικοποιείτε μόνοι σας αντί να έχει χαθεί το data. Για τις περισσότερες πραγματικές δουλειές, όμως, τα οκτώ υποστηριζόμενα φίλτρα καλύπτουν ό,τι εκδίδουν στην πράξη scanners, office suites και report engines, και ένας βρόχος πάνω από

με ένα GetLoadedImageCount guard μετατρέπει ένα loaded PDF ξανά σε έναν φάκελο από bitmaps σε λίγες γραμμές.DecodableΤο loaded-image extraction API, μαζί με το πλήρες σύνολο των decode filters που περιγράφονται εδώ, περιλαμβάνεται στο

HotPDF Component για Delphi και C++Builder. για Delphi και C++Builder