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

Απόδοση Ενσωματωμένων Γραμματοσειρών PDF στο Delphi με το HotPDF

Το HotPDF αποδίδει ενσωματωμένες γραμματοσειρές PDF στο Delphi χωρίς να εγκαταστήσει τίποτα στο μηχάνημα: ο ενσωματωμένος αγωγός απόδοσης γλύφων (glyph rendering pipeline) στο HPDFGlyphRender.pas αναλύει τα προγράμματα γραμματοσειρών που είναι αποθηκευμένα μέσα στο ίδιο το PDF — περιγράμματα TrueType glyf από το FontFile2, charstrings CFF Type 2 από το FontFile3, και ροές περιεχομένου γλύφων Type 3 — και τα αναπαράγει ως γεμάτα διανυσματικά μονοπάτια (paths) GDI. Αυτό το άρθρο αποτελεί τη βαθιά βουτιά στην πιστότητα γραμματοσειρών πίσω από την απόδοση σελίδων PDF σε ένα TBitmap με το HotPDF: εκείνο το κομμάτι καλύπτει τον renderer ως σύνολο, αυτό καλύπτει τον τρόπο με τον οποίο το κείμενο σε αυτές τις σελίδες αποκτά τα ακριβή του σχήματα

Γιατί ένα PDF αποδίδεται με κουτιά αντί για κείμενο;

Κουτάκια, κενά, ή ελαφρώς λανθασμένοι χαρακτήρες στο αποδοθέν αποτέλεσμα PDF σημαίνουν σχεδόν πάντα ότι ο renderer ζήτησε μια γραμματοσειρά από το λειτουργικό σύστημα αντί να χρησιμοποιήσει εκείνη που είναι ενσωματωμένη στο αρχείο. Το παράπονο φτάνει με τον ίδιο τρόπο κάθε φορά: το έγγραφο φαίνεται τέλειο στο μηχάνημα που το παρήγαγε, και στη συνέχεια ένας πελάτης το ανοίγει σε έναν καθαρό διακομιστή ή σε μια κλειδωμένη επιφάνεια εργασίας και το ιαπωνικό τιμολόγιο εμφανίζει τετράγωνα ("tofu"), ή μια υποκατεστημένη παρεμφερής γραμματοσειρά μετατοπίζει κάθε αλλαγή γραμμής. Οι γραμματοσειρές δεν υπήρχαν ποτέ σε εκείνο το μηχάνημα — μόνο μέσα στο PDF — και ένας renderer που σταματά στην υποκατάσταση γραμματοσειρών συστήματος δεν μπορεί να τις δει. Οι υποσύνολα γραμματοσειρές (subset fonts) το κάνουν χειρότερο: ένα υποσύνολο μπορεί να φέρει σαράντα γλύφους υπό κωδικούς χαρακτήρων που έχουν εκχωρηθεί ιδιωτικά σε αυτό το συγκεκριμένο αρχείο, μια εκχώρηση που καμία εγκατεστημένη γραμματοσειρά δεν μοιράζεται

Το πρότυπο ISO 32000-1 §9.9 ορίζει τρεις φορείς για ένα ενσωματωμένο πρόγραμμα γραμματοσειράς στον περιγραφέα γραμματοσειράς: το FontFile περιέχει ένα αρχικό πρόγραμμα Type 1, το FontFile2 ένα πρόγραμμα TrueType και το FontFile3 ένα απλό CFF (Type1C ή CIDFontType0C) ή ένα περίβλημα OpenType. Μια τέταρτη παραλλαγή, η γραμματοσειρά Type 3 του ISO 32000-1 §9.6.5, δεν ενσωματώνει τίποτα δυαδικό (binary) — κάθε γλύφος είναι μια μικρή ροή περιεχομένου PDF που εκτελείται επί τόπου. Οι τρεις φορείς διαφέρουν στα μαθηματικά των περιγραμμάτων (δευτεροβάθμιες B-splines έναντι τριτοβάθμιων charstrings έναντι αυθαίρετων τελεστών σελίδας), οπότε ένας πιστός renderer χρειάζεται έναν ξεχωριστό διερμηνέα (interpreter) για τον καθένα, συν ένα επίπεδο κωδικοποίησης που μετατρέπει τους κωδικούς χαρακτήρων στον σωστό δείκτη γλύφου πριν αγγιχτεί οποιοδήποτε περίγραμμα

Πώς μετατρέπει το HotPDF τα περιγράμματα TrueType glyf σε μονοπάτια GDI;

Το THPDFEmbeddedTTF στο HPDFGlyphRender.pas διαβάζει τον πίνακα loca για να εντοπίσει κάθε εγγραφή γλύφου, διατρέχει τα περιγράμματα (contours) glyf σημείο προς σημείο και εκπέμπει ένα μονοπάτι (path) GDI. Δύο συμβάσεις TrueType χρειάζονται ρητό χειρισμό. Πρώτον, διαδοχικά σημεία εκτός καμπύλης (off-curve points) υποδηλώνουν ένα σημείο πάνω στην καμπύλη (on-curve point) στο μέσο τους, και ένα περίγραμμα του οποίου όλα τα σημεία είναι εκτός καμπύλης ξεκινά από το μέσο του τελευταίου και του πρώτου σημείου του — αν παραλείψετε οποιονδήποτε κανόνα, οι στρογγυλεμένοι γλύφοι αποκτούν επίπεδες πτυχές ή καταρρέουν. Δεύτερον, οι καμπύλες TrueType είναι δευτεροβάθμιες Bézier, ενώ η PolyBezierTo του GDI δέχεται τριτοβάθμιες, οπότε κάθε δευτεροβάθμιο τμήμα ανυψώνεται ως προς το βαθμό του με ακρίβεια, αντί να επιπεδώνεται σε τμήματα γραμμών

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

Η ανύψωση βαθμού είναι χωρίς απώλειες (lossless): η τριτοβάθμια καμπύλη ιχνηλατεί την ίδια ακριβώς καμπύλη, οπότε το αποδοθέν περίγραμμα ταιριάζει με αυτό που σχεδιάζει ένα συμβατό πρόγραμμα προβολής από τον ίδιο πίνακα, σε οποιοδήποτε ζουμ. Η υπόλοιπη εργασία είναι η τοποθέτηση. Κάθε γλύφος είναι σχεδιασμένος σε μονάδες γραμματοσειράς (συνήθως ένα πλέγμα 1000 ή 2048 units-per-em), και ο renderer συνθέτει τον πίνακα κλίμακας (scale matrix), τον πίνακα κειμένου (text matrix) και τον τρέχοντα πίνακα μετασχηματισμού (CTM) σε έναν μετασχηματισμό γλύφου-προς-συσκευή πριν γεμίσει το μονοπάτι. Η σειρά έχει σημασία εδώ περισσότερο από ό,τι φαίνεται: αν συνθέσετε τους ίδιους τρεις πίνακες αντίστροφα, κάθε γλύφος καταρρέει προς την προέλευση — μια σελίδα που φαίνεται λανθασμένη, της οποίας το πραγματικό σφάλμα είναι μία μόνο γραμμή άλγεβρας πινάκων

Πώς ο διερμηνέας Type 2 charstring χειρίζεται γραμματοσειρές CFF

Το THPDFEmbeddedCFF παρέχει στα προγράμματα FontFile3 έναν πραγματικό διερμηνέα Type 2 charstring: αναλύει τις δομές CFF INDEX, το Top DICT και το Private DICT, και στη συνέχεια εκτελεί κάθε charstring και εκπέμπει τμήματα μονοπατιών απευθείας στο GDI. Ένα περίβλημα OpenType (ο περιέκτης OTTO) αφαιρείται πρώτα για να φτάσει στον απλό πίνακα CFF· οι γυμνές ροές CIDFontType0C και Type1C καταναλώνονται απευθείας. Τα charstrings είναι μια συμπαγής γλώσσα στοίβας (stack language), και τρεις από τις συμβάσεις της καθορίζουν εάν ο διερμηνέας παραμένει σε συγχρονισμό με τη ροή byte. Το προαιρετικό πρόθεμα πλάτους (width prefix) σημαίνει ότι ο πρώτος τελεστής εκκαθάρισης στοίβας μπορεί να φέρει έναν επιπλέον αρχικό τελεστέο (operand). Ο τελεστής hintmask υποδηλώνει ένα vstemhm όταν οι τελεστέοι βρίσκονται ακόμα στη στοίβα, και ο αριθμός των byte μάσκας προς παράκαμψη εξαρτάται από τον συσσωρευμένο αριθμό στελεχών (stem count) — αν κάνετε λάθος στον αριθμό μία φορά, κάθε επόμενος κωδικός λειτουργίας (opcode) θα διαβαστεί λάθος. Και οι κλήσεις υπορουτινών προσθέτουν μια απόκλιση (bias) στο δείκτη τους (107, 1131 ή 32768 ανάλογα με τον αριθμό των υπορουτινών) πριν από την αναζήτηση, οπότε μια κλήση χωρίς απόκλιση καταλήγει σε εντελώς λάθος υπορουτίνα

Το CFF με κλειδί CID (CID-keyed CFF) προσθέτει μια έμμεση αναφορά (indirection) που παγιδεύει τις απλοϊκές υλοποιήσεις: ο κωδικός χαρακτήρα επιλέγει ένα CID, αλλά ο δείκτης charstring είναι ένα GID, και το σύνολο χαρακτήρων (charset) της γραμματοσειράς αντιστοιχίζει το GID στο CID — έτσι ο renderer κατασκευάζει την αντίστροφη αντιστοίχιση CID-προς-GID πριν από τη σχεδίαση, και επιλέγει το Private DICT ανά γλύφο μέσω του FDSelect για γραμματοσειρές που φέρουν πολλά τέτοια. Τα προγράμματα Type1C με κλειδί ονόματος (name-keyed), ο συνήθης φορέας για απλές γραμματοσειρές Type 1, επιλύουν μονοπάιτ κωδικούς μέσω της ενσωματωμένης κωδικοποίησης του προγράμματος CFF ή μέσω του μηχανισμού κωδικοποίησης επιπέδου PDF που περιγράφεται στη συνέχεια. Μια ειλικρινής προειδοποίηση: ο διερμηνέας διαβάζει τους τελεστές hint για να διατηρήσει τη ροή συγχρονισμένη, αλλά δεν εκτελεί hinting, ένα όριο που συζητείται στο τέλος

Τι είναι μια γραμματοσειρά Type 3 και πώς σχεδιάζεται;

Ένας γλύφος Type 3 δεν είναι καθόλου περίγραμμα — το πρότυπο ISO 32000-1 §9.6.5 τον ορίζει ως ροή περιεχομένου (content stream), οπότε το HotPDF τον αποδίδει αποδίδοντας την κατάσταση γραφικών (graphics state), συνθέτοντας τον πίνακα γραμματοσειράς (font matrix), το μέγεθος γραμματοσειράς και τον πίνακα κειμένου πάνω στο CTM, και εκτελώντας τη διαδικασία γλύφου (glyph procedure) μέσω του ίδιου διερμηνέα τελεστών που σχεδιάζει τις σελίδες, με τους πόρους /Resources της ίδιας της γραμματοσειράς σε ισχύ. Δύο λεπτομέρειες των προδιαγραφών έχουν σημασία για την ορθότητα. Τα πλάτη /Widths των Type 3 εκφράζονται στο χώρο του γλύφου και όχι στο χώρο κειμένου 1/1000 που χρησιμοποιεί κάθε άλλος τύπος γραμματοσειράς, οπότε οι προχωρήσεις (advances) πρέπει να περάσουν μέσα από το /FontMatrix — οι γραμματοσειρές barcode με πίνακα 0.01 διαφορετικά βηματίζουν λάθος κατά μια τάξη μεγέθους. Και μια διαδικασία γλύφου που ανοίγει με τον τελεστή d1 δίνει δύο υποσχέσεις που επιβάλλει ο renderer: η σχεδίαση περιορίζεται (clipped) στο δηλωμένο πλαίσιο οριοθέτησης (bounding box), και σύμφωνα με το ISO 32000-1 §9.6.5.2 ο γλύφος αγνοεί τους δικούς του τελεστές χρώματος και σχεδιάζει με το τρέχον χρώμα γεμίσματος του καλούντος, οπότε οι τελεστές rg, g, k και οι αντίστοιχοι τελεστές σχεδίασης γραμμών (stroking) μέσα στη διαδικασία καταστέλλονται για τη διάρκεια αυτού του γλύφου. Αν παραλείψετε τον κανόνα χρώματος, μια γραμματοσειρά barcode d1 που σφραγίζεται με μπλε χρώμα από τη σελίδα θα βγει μαύρη· αν παραλείψετε την αποκοπή (clip), ένας κακοσχηματισμένος γλύφος θα σχεδιαστεί έξω από το κελί του

Πώς οι κωδικοί χαρακτήρων γίνονται ID γλύφων

Οι διερμηνείς περιγραμμάτων είναι μόνο η μισή δουλειά, επειδή τα byte σε μια συμβολοσειρά κειμένου PDF είναι κωδικοί χαρακτήρων και όχι δείκτες γλύφων (glyph indices), και το ISO 32000-1 αφιερώνει δύο εδάφια σε αυτήν την αντιστοίχιση. Για απλές γραμματοσειρές, η §9.6.6 ορίζει μια αυστηρή προτεραιότητα: ένας πίνακας /Differences υπερισχύει της βασικής κωδικοποίησης (WinAnsiEncoding, MacRomanEncoding, ή StandardEncoding), η οποία υπερισχύει του χάρτη του ίδιου του προγράμματος γραμματοσειράς. Το HotPDF επιλύει αυτή την αλυσίδα σε έναν πίνακα κωδικού-προς-GID 256 καταχωρίσεων, μεταφράζοντας τα ονόματα των γλύφων σε δείκτες γλύφων μέσω τριών οδών: ακριβής αντιστοίχιση charset μέσα σε ένα πρόγραμμα CFF, αριθμητικά ονόματα gNN/glyphNN που λαμβάνονται ως κυριολεκτικοί δείκτες, και μετάφραση ονόματος-προς-Unicode βάσει του Adobe Glyph List, ακολουθούμενη από αναζήτηση cmap για προγράμματα TrueType. Για σύνθετες γραμματοσειρές, η §9.7 θέτει το CIDToGIDMap επικεφαλής: η συνήθης περίπτωση είναι το /Identity, αλλά η καταχώριση μπορεί να είναι μια ροή από big-endian ζεύγη με δείκτη το CID — και η έξοδος Unicode του ίδιου του HotPDF χρησιμοποιεί ακριβώς αυτή τη μορφή ροής για συμπαγή υποσύνολα, οπότε η διαδρομή ροής δεν είναι μια εξωτική γωνιά

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

Όταν απαιτείται αναζήτηση cmap TrueType, το HotPDF διατρέχει μια αλυσίδα εναλλακτικών λύσεων (fallback chain) αντί να εμπιστεύεται έναν μόνο υποπίνακα: οι υποπίνακες Windows Unicode (μορφή 4, στη συνέχεια μορφή 12 για συμπληρωματικά επίπεδα) έρχονται πρώτοι, μετά ο υποπίνακας συμβόλων (3,0) με τη σύμβαση περιοχής ιδιωτικής χρήσης F000 αντικατοπτρισμένη στο χαμηλό byte — ο λόγος για τον οποίο μια γραμματοσειρά συμβόλων όπως η Wingdings απαντά σε απλούς κωδικούς ASCII — και στη συνέχεια οι παλαιές μορφές 6 και 0. Οι υποπίνακες μορφής 2 σκοπίμως δεν ερμηνεύονται: αντιστοιχίζουν παλαιές κωδικοσελίδες πολλαπλών byte, όπως οι Shift-JIS και Big5, και όχι Unicode, και οι σύγχρονες γραμματοσειρές CJK φέρουν πάντα έναν υποπίνακα μορφής 4 ή μορφής 12. Οποιοσδήποτε κωδικός δεν επιβιώνει από καμία από αυτές τις διαδρομές επιστρέφει στη σχεδίαση GDI για αυτόν τον μεμονωμένο γλύφο, οπότε ένας μη αντιστοιχίσιμος χαρακτήρας υποβαθμίζει έναν μόνο γλύφο, όχι ολόκληρη τη ροή κειμένου

Τι δεν κάνει η ενσωματωμένη διαδρομή

Τα όρια αξίζει να αναφερθούν ξεκάθαρα. Το hinting δεν εκτελείται — τα περιγράμματα γεμίζουν όπως έχουν σχεδιαστεί, κάτι που δεν διακρίνεται από το hinted αποτέλεσμα στα 150 DPI και άνω, αλλά μπορεί να διαφέρει κατά ένα pixel από έναν hinted rasterizer σε πολύ μικρά μεγέθη. Τα αρχικά προγράμματα Type 1 στο FontFile (eexec-κρυπτογραφημένα charstrings) δεν ερμηνεύονται, και οι άξονες μεταβλητών γραμματοσειρών (variable-font axes) OpenType δεν εφαρμόζονται· και οι δύο περιπτώσεις, όπως ένα κατεστραμμένο πρόγραμμα γραμματοσειράς ή ένας πίνακας glyf χωρίς κανέναν χρησιμοποιήσιμο cmap, επιστρέφουν στη σχεδίαση γραμματοσειρών συστήματος αντί να αποτύχει η σελίδα. Η ίδια προσέγγιση "προτεραιότητα στην πιστότητα" επεκτείνεται και αλλού στον renderer — τα μοτίβα αξονικής και ακτινικής σκίασης λαμβάνουν την ίδια μεταχείριση που αξίζουν οι διαβαθμίσεις (gradients) — και η πλευρά της δημιουργίας έχει τη δική της ιστορία λεπτομέρειας γραμματοσειρών στο πώς το EndDoc ταξινομεί τα υποσύνολα γραμματοσειρών

Η χρήση του αγωγού (pipeline) δεν απαιτεί καθόλου κώδικα ειδικό για γραμματοσειρές — κάθε παραπάνω μηχανισμός ενεργοποιείται αυτόματα μέσα στην κλήση απόδοσης της σελίδας

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

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