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

Απόδοση σελίδων PDF σε Bitmap στο Delphi με το HotPDF

Το HotPDF αποδίδει μια φορτωμένη σελίδα PDF σε ένα Delphi TBitmap μέσω μιας μόνο κλήσης: RenderLoadedPageToBitmap(PageIndex, DPI). Η συνάρτηση ερμηνεύει τη ροή περιεχομένου (content stream) της σελίδας και επιστρέφει ένα bitmap 24-bit RGB που ανήκει στον καλούντα στην ανάλυση που επιλέγετε, το οποίο είναι ακριβώς αυτό που χρειάζεται μια λωρίδα μικρογραφιών (thumbnails), μια προεπισκόπηση εκτύπωσης ή μια ροή εξαγωγής PDF σε εικόνα. Αυτό το άρθρο περιγράφει το API, και στη συνέχεια το μέρος που διαχωρίζει έναν χρησιμοποιήσιμο αποδοτικό μηχανισμό (renderer) από ένα παιχνίδι: τη σχεδίαση κειμένου από τα ίδια τα ενσωματωμένα προγράμματα γραμματοσειρών αντί για παρόμοιες γραμματοσειρές συστήματος

Γιατί η απόδοση μιας σελίδας PDF είναι πιο δύσκολη από τη σχεδίαση μιας εικόνας;

Δεν είναι μια εικόνα. Είναι ένα πρόγραμμα: μια ροή τελεστών που κατασκευάζουν διαδρομές, επιλέγουν γραμματοσειρές, ορίζουν χρώματα και τοποθετούν γλύφες, εκτελούμενη έναντι του μοντέλου γραφικών που ορίζεται στο ISO 32000-1 §8. Τίποτα στο αρχείο δεν λέει πώς φαίνεται οποιοδήποτε εικονοστοιχείο (pixel). Για να δημιουργήσετε ένα bitmap πρέπει να εκτελέσετε αυτό το πρόγραμμα — να διατηρήσετε έναν τρέχοντα πίνακα μετασχηματισμού, μια στοίβα κατάστασης γραφικών για τα q/Q, μια διαδρομή αποκοπής (clipping path), χρωματικούς χώρους γεμίσματος και περιγράμματος — και να ψηφιοποιήσετε το αποτέλεσμα. Αυτός είναι ο λόγος για τον οποίο η ενέργεια "απλώς δείξε τη σελίδα 3 ως εικόνα" είναι ένας διερμηνέας ροής περιεχομένου, όχι μια μετατροπή μορφής αρχείου

Ο renderer του HotPDF, που εισήχθη στην έκδοση v2.253.0, είναι κατασκευασμένος ως έξι αποσυζευγμένες μονάδες που αντικατοπτρίζουν αυτό το μοντέλο: ένας πυρήνας συγγενούς πίνακα (affine-matrix) για την άλγεβρα μετασχηματισμού PDF [a b c d e f], μια στοίβα κατάστασης γραφικών (graphics-state stack), ένας επιλυτής χρωματικών χώρων (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), ένας κατασκευαστής διαδρομών που γεφυρώνει τους τελεστές διαδρομή PDF με το GDI, ένα επίπεδο μετρικών γραμματοσειράς που διαβάζει πίνακες /Widths για σωστές προωθήσεις, και ο διερμηνέας που αποστέλλει τελεστές και οδηγεί τους άλλους πέντε. Τα Image XObjects περνούν από την ίδια στοίβα αποκωδικοποίησης που χρησιμοποιεί η βιβλιοθήκη για εξαγωγή, οπότε κάθε φίλτρο εικόνας που μπορεί να αποκωδικοποιήσει το HotPDF για εξαγωγή — συμπεριλαμβανομένων των εικόνων JPEG 2000 με συμπίεση JPXDecode — εμφανίζεται επίσης στο αποτέλεσμα της απόδοσης

Απόδοση μιας φορτωμένης σελίδας σε TBitmap

Η RenderLoadedPageToBitmap λαμβάνει έναν δείκτη σελίδας με βάση το μηδέν και μια τιμή DPI, όπου τα 72 DPI αντιστοιχίζουν μία μονάδα user-space του PDF σε ένα εικονοστοιχείο. Επιστρέφει nil σε περίπτωση αποτυχίας (δείκτης εκτός ορίων, έλλειψη πόρων) αντί να εγείρει εξαίρεση, έτσι ώστε μια εφαρμογή προβολής να μπορεί να παρακάμψει μια κατεστραμμένη σελίδα και να συνεχίσει. Το bitmap που επιστρέφεται ανήκει στον καλούντα, ο οποίος πρέπει να το ελευθερώσει

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // page 1 at 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Το όρισμα DPI κάνει τη δουλειά της κλιμάκωσης για κάθε κοινό σενάριο. Μια λωρίδα μικρογραφιών αποδίδεται στα 36 ή 48 DPI και παράγει μικρά, γρήγορα bitmaps· μια προεπισκόπηση στην οθόνη στα 96 ή 144 DPI ταιριάζει με την τυπική πυκνότητα της οθόνης· μια διαδρομή εξαγωγής στα 300 DPI παράγει εικόνες ποιότητας εκτύπωσης. Η περιστροφή σελίδας από την καταχώριση /Rotate και η αναστροφή προέλευσης του /MediaBox (το PDF θέτει την προέλευση κάτω-αριστερά, το GDI πάνω-αριστερά) αντιμετωπίζονται εντός του πίνακα σελίδας-προς-συσκευή, οπότε μια σελίδα US Letter στα 72 DPI επιστρέφεται ως ακριβώς 612×792 pixels με τον σωστό προσανατολισμό

Γιατί οι αποδοθείσες μικρογραφίες PDF εμφανίζουν λάθος γλύφες;

Οι λανθασμένες ή κατά προσέγγιση γλύφοι στο αποτέλεσμα της απόδοσης PDF σημαίνουν σχεδόν πάντα ότι ο renderer αντικαθιστά μια γραμματοσειρά συστήματος αντί να χρησιμοποιεί τη γραμματοσειρά που είναι ενσωματωμένη στο αρχείο. Ο πρώτος renderer του HotPDF έκανε ακριβώς αυτό: αφαιρούσε το πρόθεμα υποσυνόλου από το /BaseFont (μετατρέποντας το ABCDEF+Arial σε Arial), ζητούσε από το GDI μια γραμματοσειρά συστήματος με αυτό το όνομα και σχεδίαζε το κείμενο με αυτήν. Για ένα έγγραφο που χρησιμοποιεί Arial ή Times New Roman με τυπική κωδικοποίηση, το αποτέλεσμα φαίνεται κοντινό. Αλλά είναι μια προσέγγιση, και αποτυγχάνει με πολύ συγκεκριμένους τρόπους

Οι ενσωματωμένες γραμματοσειρές υποσυνόλου είναι η χειρότερη περίπτωση. Μια γραμματοσειρά υποσυνόλου μπορεί να φέρει μόνο τις σαράντα γλύφες που χρησιμοποιεί πραγματικά ένα έγγραφο, με κωδικούς χαρακτήρων που έχουν εκχωρηθεί σε μια σειρά ιδιωτική για το συγκεκριμένο αρχείο — ο κωδικός 1 μπορεί να είναι το "T", ο κωδικός 2 το "h" και ούτω καθεξής. Μια γραμματοσειρά συστήματος δεν γνωρίζει τίποτα για αυτήν την ιδιωτική εκχώρηση, οπότε το κείμενο είτε εξαφανίζεται είτε εμφανίζεται ως εντελώς λανθασμένοι χαρακτήρες. Οι προσαρμοσμένες κωδικοποιήσεις, οι γραμματοσειρές συμβόλων, οι γραμματοσειρές barcode και οποιαδήποτε γραμματοσειρά που δεν είναι εγκατεστημένη στο μηχάνημα απόδοσης αποτυγχάνουν με τον ίδιο τρόπο. Ένας renderer που σταματά στην αντικατάσταση γραμματοσειράς συστήματος παράγει μικρογραφίες που αναγνωρίζονται ως η σελίδα — μέχρι η σελίδα να χρησιμοποιήσει τις γραμματοσειρές που κατέστησαν εξαρχής απαραίτητη την ενσωμάτωση

Απόδοση ενσωματωμένων γλυφών: σχεδίαση από το ίδιο το πρόγραμμα γραμματοσειράς

Το HotPDF γεφύρωσε αυτό το χάσμα σε πέντε εκδόσεις (v2.268.0 έως v2.272.0) αναλύοντας τα ενσωματωμένα προγράμματα γραμματοσειρών και αναπαράγοντας τα περιγράμματα των γλυφών τους ως γεμισμένες διανυσματικές διαδρομές GDI. Το κείμενο σε μια αποδοθείσα σελίδα προέρχεται τώρα από τα ίδια δεδομένα περιγράμματος που χρησιμοποιεί μια συμμορφούμενη εφαρμογή προβολής, πράγμα που σημαίνει ότι οι γραμματοσειρές υποσυνόλου, οι προσαρμοσμένες κωδικοποιήσεις και οι μη εγκατεστημένες γραμματοσειρές αποδίδονται με τα ακριβή τους σχήματα. Η κάλυψη δημιουργήθηκε ανά τύπο γραμματοσειράς:

Για γραμματοσειρές Type0/CIDFontType2 με ενσωματωμένο πρόγραμμα TrueType (FontFile2), ο renderer αναλύει απευθείας τους πίνακες glyf και loca: τα τετραγωνικά περιγράμματα μετατρέπονται στα κυβικά Béziers που κατανοεί το GDI, τα υπονοούμενα on-curve σημεία μεταξύ διαδοχικών off-curve σημείων ανακατασκευάζονται, και οι σύνθετες γλύφοι αναπαράγονται αναδρομικά. Υποστηρίζονται τόσο οι διατάξεις Identity όσο και οι ρητές ροές CIDToGIDMap, και οι προωθήσεις CID σέβονται τις καταχωρίσεις πλάτους /W και /DW, έτσι ώστε το κείμενο Identity-H δύο byte να προχωρά σωστά

Τα προγράμματα CFF (FontFile3, είτε CIDFontType0C, Type1C, είτε ένα περιτύλιγμα OpenType) λαμβάνουν έναν πλήρη διερμηνέα charstring Type 2: γραμμές, καμπύλες, την οικογένεια flex, μάσκες υποδείξεων (hint masks) και τοπικές/καθολικές κλήσεις υπορουτινών με τη σωστή μεροληψία υπορουτίνας. Τα προγράμματα CFF με κλειδί CID αντιστοιχίζουν τους κωδικούς χαρακτήρων μέσω του συνόλου χαρακτήρων της γραμματοσειράς, κάτι που έχει σημασία για γραμματοσειρές υποσυνόλου των οποίων η σειρά γλυφών διαφέρει από τη σειρά CID, και η επιλογή font-DICT ανά γλύφο μέσω FDArray/FDSelect τηρείται. Οι απλές (μη CID) γραμματοσειρές TrueType επιλύουν κωδικούς ενός byte μέσω του πίνακα cmap της ίδιας της ενσωματωμένης γραμματοσειράς με μια ισχυρή αλυσίδα υποπινάκων — μορφές Unicode 4 και 12 πρώτα, στη συνέχεια υποπίνακες συμβόλων με τον καθρέφτη ιδιωτικής χρήσης F000, και μετά παλαιές μορφές Macintosh — ενώ οι απλές γραμματοσειρές Type1 επιλύονται μέσω της ενσωματωμένης κωδικοποίησης του προγράμματος CFF

Δύο βελτιώσεις συμπληρώνουν την εικόνα. Πρώτον, τα λεξικά /Encoding απλών γραμματοσειρών επιλύονται σύμφωνα με την προτεραιότητα που προδιαγράφει το ISO 32000-1 §9.6.6: οι πίνακες /Differences υπερισχύουν της βασικής κωδικοποίησης, η οποία με τη σειρά της υπερισχύει του χάρτη του ίδιου του προγράμματος γραμματοσειράς — η διαδρομή από την οποία εξαρτώνται οι αλυσίδες εργαλείων που προέρχονται από το TeX και το PostScript, με τα ονόματα των γλυφών να επιλύονται μέσω της λίστας γλυφών της Adobe (Adobe Glyph List), του συνόλου χαρακτήρων CFF ή του TrueType cmap. Δεύτερον, οι γραμματοσειρές Type3, των οποίων οι γλύφοι είναι οι ίδιες μικρές ροές περιεχομένου, αναπαράγονται μέσω του renderer με τον πίνακα γραμματοσειράς, το μέγεθος γραμματοσειράς και τον πίνακα κειμένου συντεθειμένα· τα /Widths του χώρου των γλυφών ερμηνεύονται μέσω του /FontMatrix όπως απαιτεί το ISO 32000-1 §9.6.5, και οι διαδικασίες γλυφών που δηλώνουν ένα πλαίσιο οριοθέτησης d1 περικόπτονται σε αυτό, οπότε μια κακοσχηματισμένη γλύφος barcode δεν μπορεί να σχεδιάσει έξω από το κελί της. Όταν ένας κωδικός δεν μπορεί να αντιστοιχιστεί — ένα κατεστραμμένο πρόγραμμα, ένας μη αντιστοιχισμένος χαρακτήρας — ο renderer καταφεύγει στη σχεδίαση γραμματοσειράς συστήματος για τη συγκεκριμένη γλύφο αντί να απορρίψει ολόκληρο το τμήμα κειμένου

Πώς κάνετε γρήγορες τις επαναλαμβανόμενες αποδόσεις;

Η απάντηση που αποστέλλει το HotPDF είναι μια προσωρινή μνήμη (cache) των πιο πρόσφατα χρησιμοποιημένων σελίδων: Η RenderLoadedPageToBitmapCached διατηρεί έως και RenderCacheCapacity αποδοθείσες σελίδες (προεπιλογή 8) με κλειδί τον δείκτη σελίδας και το DPI, και μια επιτυχία στην cache επιστρέφει ένα νέο αντίγραφο που ανήκει στον καλούντα χωρίς να αγγίξει τη ροή περιεχομένου — συνήθως χιλιάδες φορές γρηγορότερα από την επανερμηνεία της σελίδας. Αυτό το μοτίβο ταιριάζει ακριβώς στις εφαρμογές προβολής: ένας χρήστης που αλλάζει μεταξύ δύο σελίδων, ή ένα συμβάν αλλαγής μεγέθους που ζητά ξανά την ίδια σελίδα στο ίδιο DPI, βρίσκει τα δεδομένα στην cache κάθε φορά

// Thumbnail strip: first pass renders, scrolling back hits the cache
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// After editing a loaded page in place:
Pdf.InvalidateRenderedPageCache;  // next render reflects the change

Να είστε ειλικρινείς με το κόστος μνήμης πριν αυξήσετε τη χωρητικότητα. Μια σελίδα US Letter στα 300 DPI είναι 2550×3300 εικονοστοιχεία, περίπου 25 MB ως bitmap 24-bit, οπότε οκτώ προσωρινά αποθηκευμένες σελίδες σε ανάλυση εξαγωγής καταλαμβάνουν περίπου 200 MB. Στο DPI των μικρογραφιών, οι ίδιες οκτώ καταχωρίσεις κοστίζουν πολύ λιγότερο από ένα megabyte. Ρυθμίστε το μέγεθος του RenderCacheCapacity για το DPI στο οποίο πραγματικά αποθηκεύετε προσωρινά, και καλέστε την InvalidateRenderedPageCache μετά από οποιαδήποτε επιτόπια επεξεργασία — η προσωρινή μνήμη έχει ως κλειδί μόνο τη σελίδα και το DPI, και δεν μπορεί να δει ότι το υποκείμενο περιεχομένο άλλαξε. Η φόρτωση ενός νέου εγγράφου την εκκαθαρίζει αυτόματα

Μια δεύτερη cache λειτουργεί κάτω από την cache σελίδων: τα αποκωδικοποιημένα Image XObjects διατηρούνται σε έναν χώρο αποθήκευσης με περιορισμό byte που οριοθετείται από το ImageCacheMaxBytes (προεπιλογή 32 MB) με εκδίωξη των λιγότερο πρόσφατα χρησιμοποιημένων (LRU). Ένα λογότυπο ή μια εικόνα επιστολόχαρτου που επαναλαμβάνεται σε κάθε σελίδα αποκωδικοποιείται μία φορά ανά φόρτωση εγγράφου αντί για μία φορά ανά τελεστή Do, γεγονός που μειώνει περίπου στο μισό το χρόνο απόδοσης για σελίδες με κοινόχρηστες εικόνες και επιταχύνει την εξαγωγή TIFF πολλαπλών σελίδων κατά το ίδιο μέτρο. Η InvalidateRenderedPageCache εκκαθαρίζει και αυτή την προσωρινή μνήμη

Τι εξακολουθεί να αποδίδεται κατά προσέγγιση

Ο renderer στοχεύει στο κοινό υποσύνολο εγγράφων PDF, και αξίζει να γνωρίζετε πού βρίσκονται τα όρια. Οι χρωματικοί χώροι CalRGB, Lab και ICC προσεγγίζονται αντί να υπόκεινται σε διαχείριση χρώματος — οι χρωματικοί χώροι συσκευής, οι παλέτες Indexed και οι δειγματοληπτικές αναζητήσεις χρωμάτων συναρτήσεων Τύπου 0 υποστηρίζονται, αλλά ένα αρχείο παραγωγής εκτύπωσης που βασίζεται σε προθέσεις απόδοσης ICC δεν θα είναι χρωματομετρικά ακριβές. Τα μοτίβα σκίασης (sh) και οι λειτουργίες ανάμειξης (blend modes) πέρα από το απλό alpha είναι επίσης εκτός σκοπού, και η αναδρομή Form XObject είναι περιορισμένου βάθους ως προστασία κύκλου. Για τιμολόγιο, αναφορές, συμβόλαια και φόρμες — σελίδες που αποτελούνται από κείμενο, διαδρομές και εικόνες — το αποτέλεσμα είναι πιστό· για ένα δοκίμιο σχεδίου γεμάτο διαβαθμίσεις και ομάδες διαφάνειας, αντιμετωπίστε το bitmap ως προεπισκόπηση, όχι ως δοκίμιο

Η πρακτική ανάγνωση: εάν η ροή εργασίας σας παράγει έγγραφα με το HotPDF ή καταναλώνει τυπικά επιχειρηματικά PDF, η RenderLoadedPageToBitmap τα αποδίδει με τα ακριβή σχήματα των ενσωματωμένων γλυφών, τις σωστές προωθήσεις CID και τη σωστή γεωμετρία σελίδας. Οι προσεγγίσεις ζουν στις γωνίες του μοντέλου γραφικών που τα επιχειρηματικά έγγραφα σπάνια επισκέπτονται

Η RenderLoadedPageToBitmap, η προσωρινά αποθηκευμένη παραλλαγή της, και η περιγραφόμενη εδώ ροή εργασίας απόδοσης ενσωματωμένων γλυφών αποστέλλονται ως μέρος του HotPDF Component για Delphi και C++Builder — μια εγγενής βιβλιοθήκη VCL χωρίς εξωτερικές εξαρτήσεις DLL, που καλύπτει τη δημιουργία, επεξεργασία, εξαγωγή κειμένου και απόδοση σελίδων PDF σε ένα ενιαίο πακέτο