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

Γραμματοσειρές και Κείμενο PDF: Γιατί τα Γλυφικά Γίνονται Κουτάκια

Ένα PDF που φαίνεται τέλειο στο μηχάνημά σας και αποδίδεται ως μια σειρά από άδεια κουτιά σε κάποιο άλλο είναι το πιο συνηθισμένο ελάττωμα γραμματοσειράς στο λογισμικό εγγράφων και σχεδόν ποτέ δεν σημαίνει ότι το κείμενο είναι λάθος. Οι χαρακτήρες είναι άθικτοι, η κωδικοποίηση είναι εντάξει, τα γλυφικά (glyphs) απλώς δεν υπάρχουν. Αυτό που άλλαξε μεταξύ των δύο μηχανημάτων είναι το ποιες γραμματοσειρές είχε εγκαταστήσει το λειτουργικό σύστημα, και το κενό μεταξύ ενός φορητού αρχείου και ενός εύθραυστου είναι μία απόφαση που ελήφθη όταν γράφτηκε η σελίδα: εάν η γραμματοσειρά ταξίδεψε μέσα στο PDF ή θεωρήθηκε παρούσα στην άλλη άκρη

Η κατανόηση του γιατί συμβαίνει αυτό και γιατί μια ξεχωριστή αποτυχία παράγει κείμενο που φαίνεται αναζητήσιμο αλλά αντιγράφεται ως ακαταλαβίστικα, σημαίνει να εξετάσουμε πώς το PDF αποθηκεύει το κείμενο. Δεν αποθηκεύει προτάσεις. Αποθηκεύει κωδικούς γλυφικών συν ένα πρόγραμμα γραμματοσειράς συν πίνακες που αντιστοιχούν το ένα στο άλλο, και κάθε σφάλμα απόδοσης ή εξαγωγής ζει σε ένα κενό μεταξύ αυτών των τριών. Ακολουθεί μια περιήγηση σε αυτόν τον μηχανισμό, βασισμένη στο ISO 32000, με τις κλήσεις Delphi που τον ελέγχουν όπου έχουν σημασία

Χαρακτήρες, κωδικοί και γλυφικά είναι τρία διαφορετικά πράγματα

Το λεξιλόγιο μπερδεύει τους ανθρώπους επειδή η καθημερινή ομιλία συμπτύσσει τρεις ξεχωριστές ιδέες στη λέξη "γράμμα". Ένας χαρακτήρας είναι μια αφηρημένη μονάδα γραφής, η ιδέα του κεφαλαίου Α, που προσδιορίζεται στο Unicode ως U+0041. Ένα γλυφικό (glyph) είναι ένα σχεδιασμένο σχήμα, το περίγραμμα καμπύλης-και-στελέχους (curve-and-stem) που χρησιμοποιεί μια συγκεκριμένη γραμματοσειρά για να απεικονίσει αυτόν τον χαρακτήρα. Ανάμεσά τους κάθεται ο κωδικός: το byte ή τα bytes στη ροή περιεχομένου που λένε στο πρόγραμμα προβολής ποιο γλυφικό στην τρέχουσα γραμματοσειρά να ζωγραφίσει

Το PDF λειτουργεί με κωδικούς. Όταν μια ροή περιεχομένου εμφανίζει μια συμβολοσειρά, αυτά τα byte είναι δείκτες στην ενεργή γραμματοσειρά, όχι Unicode. Η κωδικοποίηση της γραμματοσειράς αποφασίζει ότι ο κωδικός 65 σημαίνει "σχεδίασε το γλυφικό που έχει καταχωρηθεί στο 65", και τίποτα σε αυτήν τη λειτουργία δεν γνωρίζει ότι το αποτέλεσμα μοιάζει με το γράμμα A σε έναν άνθρωπο. Αυτό είναι που κάνει το PDF να αποδίδεται πανομοιότυπα παντού όπου μπορεί να βρει τα γλυφικά, και είναι επίσης ο λόγος που η εξαγωγή (extraction) είναι ξεχωριστό πρόβλημα από την εμφάνιση: η σχεδίαση χρειάζεται μόνο αντιστοίχιση κωδικού-σε-γλυφικό (code-to-glyph), η ανάγνωση χρειάζεται κωδικού-σε-Unicode, και αυτοί είναι δύο διαφορετικοί πίνακες που μπορούν να διαφωνούν ή να λείπουν ανεξάρτητα

Οι τύποι γραμματοσειρών που πραγματικά θα συναντήσετε

Το ISO 32000 ορίζει διάφορους τύπους λεξικών γραμματοσειρών και στην πράξη ένα έγγραφο που λαμβάνετε ή δημιουργείτε χρησιμοποιεί έναν από τους τρεις. Γνωρίζοντας ποιον κοιτάτε εξηγεί τα περισσότερα από τα πράγματα που μπορεί να πάνε στραβά

Η Type 1 είναι η αρχική μορφή περιγράμματος PostScript της Adobe, κατασκευασμένη από κυβικές καμπύλες Bezier. Οι δεκατέσσερις τυπικές γραμματοσειρές που πρέπει να παρέχει κάθε συμβατός αναγνώστης, οι οικογένειες Helvetica, Times, Courier, Symbol και ZapfDingbats, είναι Type 1 και ένα λεξικό γραμματοσειράς που ονομάζει μία από αυτές μπορεί νομίμως να παραλείψει το πρόγραμμα γραμματοσειράς. Αυτή είναι η μόνη περίπτωση όπου το να αφήσετε μια γραμματοσειρά μη ενσωματωμένη (unembedded) είναι ασφαλές βάσει προδιαγραφών και όχι από τύχη. Για οποιαδήποτε άλλη γραμματοσειρά Type 1, το πρόγραμμα πρέπει να είναι ενσωματωμένο ή το πρόγραμμα προβολής αντικαθιστά κάτι, συνήθως μια μετρικά παρόμοια αλλά οπτικά διαφορετική γραμματοσειρά

Η TrueType χρησιμοποιεί τετραγωνικές καμπύλες (quadratic curves) και προήλθε από τον κόσμο της Apple και της Microsoft. Είναι αυτό που είναι οι περισσότερες γραμματοσειρές συστήματος, και αυτό που θα ενσωματώνετε πιο συχνά. Μια απλή γραμματοσειρά TrueType στο PDF περιορίζεται σε κωδικούς ενός byte (single-byte), επομένως μία τέτοια γραμματοσειρά μπορεί να διευθυνσιοδοτήσει το πολύ 256 γλυφικά τη φορά. Αυτό το όριο είναι ο δομικός λόγος που οι CJK και άλλα μεγάλα αλφάβητα δεν μπορούν να βασιστούν σε μια απλή γραμματοσειρά

Η Type 0, η σύνθετη ή γραμματοσειρά με κλειδί CID, είναι η απάντηση σε αυτό το όριο. Χρησιμοποιεί κωδικούς πολλών byte (multi-byte) και έναν CMap για να τους δρομολογήσει μέσω μιας γραμματοσειράς-απογόνου CIDFont, της οποίας τα περιγράμματα είναι είτε TrueType είτε CFF/Type 1. Αυτός είναι ο μόνος τύπος γραμματοσειράς που μπορεί να μεταφέρει χιλιάδες γλυφικά, επομένως οποιοδήποτε PDF περιέχει Κινεζικά, Ιαπωνικά, Κορεατικά ή μια ευρεία πολύγλωσση μίξη χρησιμοποιεί Type 0, είτε ο συγγραφέας το σκέφτηκε είτε όχι. Το αντίτιμο είναι η πολυπλοκότητα: περισσότερα κινούμενα μέρη, περισσότερα από τα οποία πρέπει να είναι σωστά τόσο για την απόδοση όσο και για την εξαγωγή

One TrueType font rendered at 12, 18, 24, and 36 points in a PDF, showing that a single embedded outline scales to any size

Μία λεπτομέρεια πίσω από αυτήν την εικόνα καθορίζει το μέγεθος του αρχείου. Μια γραμματοσειρά είναι μια βιβλιοθήκη περιγραμμάτων, όχι bitmaps σταθερού μεγέθους, επομένως το ίδιο ενσωματωμένο πρόγραμμα εξυπηρετεί κάθε μέγεθος στιγμής (point size) στη σελίδα. Η κλιμάκωση (scaling) είναι ένας μετασχηματισμός που εφαρμόζεται τη στιγμή της σχεδίασης, γι' αυτό και μια επικεφαλίδα και το κυρίως κείμενό της μοιράζονται μία ενσωματωμένη γραμματοσειρά και ο λόγος για τον οποίο το κόστος ενσωμάτωσης αφορά τη γραμματοσειρά, όχι το μέγεθος

Η ενσωμάτωση είναι η διαφορά μεταξύ φορητού και εύθραυστου

Η ενσωμάτωση σημαίνει ότι το πρόγραμμα της γραμματοσειράς, τα πραγματικά δεδομένα περιγράμματος, είναι γραμμένα στο PDF ως ροή (stream). Ένας αναγνώστης σε ένα μηχάνημα που δεν έχει ακούσει ποτέ για τη γραμματοσειρά σας διαβάζει αυτά τα περιγράμματα κατευθείαν από το αρχείο και σχεδιάζει ακριβή γλυφικά. Παραλείψτε την ενσωμάτωση και στοιχηματίζετε ότι ο προορισμός έχει μια γραμματοσειρά με το ίδιο όνομα· όταν δεν το έχει, το πρόγραμμα προβολής επιστρέφει σε ένα υποκατάστατο. Για τις τυπικές δεκατέσσερις γραμματοσειρές αυτή η αντικατάσταση είναι καθορισμένη και ακίνδυνη. Για οτιδήποτε άλλο, κυμαίνεται από μια μικρή αστοχία σε διαφορετική γραμματοσειρά έως το αποτέλεσμα άδειου κουτιού όταν κανένα υποκατάστατο δεν καλύπτει το αλφάβητο καθόλου

Με το HotPDF ο έλεγχος είναι μια μεμονωμένη ιδιότητα, η οποία ορίζεται πριν ανοίξει το έγγραφο. Το FontEmbedding λέει στη βιβλιοθήκη να πακετάρει τις γραμματοσειρές με τις οποίες σχεδιάζει, μέσα στο αρχείο:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Compression := cmFlateDecode;
    Pdf.FontEmbedding := True;          // outlines travel inside the file
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Calibri', [], 11);
    Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Η σειρά δεν είναι διακοσμητική. Το BeginDoc είναι το σημείο όπου το HotPDF δεσμεύει τη δομή του εγγράφου, επομένως το FontEmbedding πρέπει να είναι true (αληθές) πριν από αυτήν την κλήση. Ορίστε το μετά και δεν υπάρχει κανένα σφάλμα, καμία προειδοποίηση, μόνο ένα αρχείο που αθόρυβα βγήκε χωρίς τις γραμματοσειρές του. Αυτό είναι το χειρότερο είδος σφάλματος: περνά κάθε δοκιμή στο μηχάνημα του προγραμματιστή, όπου η γραμματοσειρά τυγχάνει να είναι εγκατεστημένη, και εμφανίζεται μόνο στου πελάτη, όπου δεν είναι

Η ενσωμάτωση είναι επίσης το σημείο όπου η αδειοδότηση συναντά τη μηχανική. Ένα πρόγραμμα γραμματοσειράς φέρει σημαίες (flags) που περιγράφουν εάν μπορεί να ενσωματωθεί ελεύθερα, μόνο για προεπισκόπηση, ή καθόλου. Ο σεβασμός αυτών των σημαιών είναι δική σας ευθύνη, όχι του renderer, και το "δούλεψε" δεν είναι το ίδιο με το "επιτρεπόταν"

Δημιουργία υποσυνόλων (Subsetting): ενσωματώστε μόνο τα γλυφικά που χρησιμοποιήσατε

Η πλήρης ενσωμάτωση γράφει ολόκληρο το πρόγραμμα της γραμματοσειράς στο αρχείο. Μια μεγάλη γραμματοσειρά CJK TrueType μπορεί να φτάσει σε πολλά megabytes, και η πλήρης ενσωμάτωσή της για να δείξει δώδεκα χαρακτήρες είναι σπατάλη με τρόπο που συνδυάζεται σε ένα έγγραφο πολλών σελίδων. Η δημιουργία υποσυνόλων το λύνει αυτό γράφοντας μόνο τα γλυφικά στα οποία αναφέρεται το έγγραφο και στη συνέχεια μετονομάζοντας τη γραμματοσειρά με μια ετικέτα έξι γραμμάτων και ένα σύμβολο συν (plus), τη μορφή ABCDEF+Calibri στη λίστα γραμματοσειρών οποιουδήποτε PDF με υποσύνολα, ώστε ο αναγνώστης να μην μπερδεύει ποτέ τη μερική γραμματοσειρά με μια πλήρη γραμματοσειρά συστήματος του ίδιου ονόματος

Για τα περισσότερα παραγόμενα έγγραφα η δημιουργία υποσυνόλων είναι η σωστή προεπιλογή. Διατηρεί το μέγεθος του αρχείου ανάλογο με το περιεχόμενο αντί για τη γραμματοσειρά προέλευσης, κάτι που έχει μεγαλύτερη σημασία για τις μεγάλες πολύγλωσσες γραμματοσειρές που διαφορετικά θα κυριαρχούσαν στο αρχείο. Η μόνη προειδοποίηση είναι ότι ένα υποσύνολο περιέχει μόνο ό,τι χρησιμοποιήθηκε τη στιγμή της δημιουργίας. Εάν μια μεταγενέστερη διαδικασία προσπαθήσει να προσθέσει κείμενο σε μια υποσύνολο γραμματοσειράς αργότερα, τα γλυφικά που χρειάζεται μπορεί να μην βρίσκονται στο αρχείο, ένας πραγματικός περιορισμός στη σταδιακή επεξεργασία του PDF κάποιου άλλου

Unicode γραμματοσειρές και το πρόβλημα των CJK κουτιών

Όταν το κείμενο δεν είναι απλά Λατινικά, η διαδρομή της απλής γραμματοσειράς εξαντλείται, και η λύση είναι να εγγράψετε ρητά μια γραμματοσειρά με δυνατότητα Unicode και να αφήσετε το HotPDF να δημιουργήσει μια γραμματοσειρά Type 0 από αυτήν. Το RegisterUnicodeTTF φορτώνει ένα αρχείο TrueType μέσω της διαδρομής (path)· μετά από αυτό, το καταχωρημένο όνομα μπορεί να χρησιμοποιηθεί στο SetFont όπως οποιοδήποτε άλλο:

Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;

Δύο πράγματα το καθορίζουν αυτό. Η γραμματοσειρά πρέπει να καλύπτει τα αλφάβητα στη συμβολοσειρά (string): μια TrueType μόνο για Λατινικά δεν θα "αναπτύξει" Κινέζικα γλυφικά επειδή της το ζητήσατε, και το αποτέλεσμα είναι πάλι άδεια κουτιά, αυτή τη φορά επειδή το γλυφικό πραγματικά δεν υπάρχει σε αυτήν τη γραμματοσειρά. Και η ενσωμάτωση πρέπει να παραμείνει ενεργοποιημένη, επειδή μια γραμματοσειρά Type 0 που συναρμολογείται από ένα καταχωρημένο TTF δεν έχει νόημα για έναν αναγνώστη που δεν μπορεί να βρει τα περιγράμματα. Για μικτό περιεχόμενο, η ανθεκτική επιλογή είναι μια γραμματοσειρά ευρείας κάλυψης, με τις οικογένειες Noto και Arial Unicode MS να είναι οι συνήθεις απαντήσεις, ενσωματωμένες και σε υποσύνολο (subsetted)

Τα αλφάβητα από δεξιά προς τα αριστερά και τα πολύπλοκα αλφάβητα προσθέτουν ένα επίπεδο διαμόρφωσης (shaping) πάνω από την κάλυψη. Το HotPDF εκθέτει το RtLTextOut για τα Αραβικά και τα Εβραϊκά, το οποίο χειρίζεται την αναδιάταξη κατεύθυνσης, έτσι ώστε να περνάτε λογική σειρά και να αφήνετε τη βιβλιοθήκη να το τοποθετήσει. Για να πετύχετε τα Αραβικά χρειάζεστε κάλυψη συν διαμόρφωση συν κατεύθυνση, τρία ξεχωριστά πράγματα, και ένα κουτί εκεί μπορεί να σημαίνει ότι οποιοδήποτε από αυτά απέτυχε

Ο πίνακας ToUnicode: πού ζει το copy-paste

Όλα τα παραπάνω αφορούν τη σχεδίαση. Η εξαγωγή (extraction) είναι η κατοπτρική εικόνα και αποτυγχάνει για τους δικούς της λόγους. Ένα πρόγραμμα προβολής αποδίδει μια σελίδα χρησιμοποιώντας την αντιστοίχιση κωδικού-σε-γλυφικό της γραμματοσειράς, αλλά όταν ένας χρήστης επιλέγει κείμενο και το αντιγράφει, το πρόγραμμα προβολής πρέπει να μετατρέψει αυτούς τους ίδιους κωδικούς ξανά σε Unicode. Αυτή η αντίστροφη αντιστοίχιση είναι το CMap ToUnicode, μια προαιρετική ροή συνδεδεμένη με τη γραμματοσειρά

Όταν είναι παρόν και σωστό, το αντιγραμμένο κείμενο βγαίνει ως οι σωστοί χαρακτήρες. Όταν απουσιάζει ή είναι λάθος, ή η γραμματοσειρά ήταν υποσύνολο με προσαρμοσμένους κωδικούς γλυφικών και δεν γράφτηκε κανένα ToUnicode, η σελίδα φαίνεται τέλεια και το πρόχειρο γεμίζει με σκουπίδια: οι κωδικοί γλυφικών διαβάζονται σαν να ήταν Unicode, πράγμα που δεν είναι για ένα προσαρμοσμένα κωδικοποιημένο υποσύνολο. Αυτός είναι ο λόγος για τον οποίο ένα σαρωμένο έγγραφο με ένα επίπεδο κειμένου OCR μπορεί να είναι αναζητήσιμο, ενώ ένα ψηφιακά γεννημένο PDF από έναν απρόσεκτο δημιουργό (generator) δεν είναι. Η απόδοση και η εξαγωγή βασίζονται σε διαφορετικούς πίνακες, επομένως ένα αρχείο μπορεί να ικανοποιεί τον ένα και να αποτυγχάνει στον άλλον. Εάν η εξαγωγή έχει σημασία για την παραγωγή σας, αντιμετωπίστε έναν σωστό χάρτη ToUnicode ως απαίτηση και επαληθεύστε τον αντιγράφοντας κείμενο από ένα δείγμα αντί να πιστεύετε ότι υπάρχει

Πώς να διαγνώσετε ένα σφάλμα γραμματοσειράς γρήγορα

Ο τρόπος αποτυχίας σας λέει πού να κοιτάξετε. Τα άδεια κουτιά σε άλλο μηχάνημα σχεδόν πάντα σημαίνουν μια γραμματοσειρά που δεν ήταν ενσωματωμένη, επομένως ελέγξτε πρώτα την ενσωμάτωση και μετά την κάλυψη των γλυφικών. Κουτιά που εμφανίζονται ακόμη και στο δικό σας μηχάνημα δείχνουν στην κάλυψη: η γραμματοσειρά δεν περιέχει αυτό το αλφάβητο, ανεξάρτητα από την ενσωμάτωση. Κείμενο που αποδίδεται σωστά αλλά αντιγράφεται ως ακαταλαβίστικα είναι ένα πρόβλημα του ToUnicode, όχι πρόβλημα απόδοσης, και το να "παίζετε" με γραμματοσειρές ή την ενσωμάτωση δεν θα το διορθώσει, επειδή η σχεδίαση δεν χάλασε ποτέ. Για να διαβάσετε ένα τελειωμένο αρχείο, ανοίξτε το στο Acrobat και κοιτάξτε στις Ιδιότητες Εγγράφου (Document Properties), Γραμματοσειρές: μια υγιής καταχώριση δείχνει τον τύπο, λέει Embedded (Ενσωματωμένη) ή Embedded Subset (Ενσωματωμένο Υποσύνολο) και ονομάζει την κωδικοποίηση. Μια γραμματοσειρά που θα έπρεπε να είναι ενσωματωμένη και δεν είναι, ανακοινώνεται εκεί πριν το κάνει ένας πελάτης

Τίποτα από αυτά δεν είναι εξωτικό μόλις γίνει σαφής ο διαχωρισμός μεταξύ χαρακτήρα, κωδικού και γλυφικού. Ενσωματώστε τις γραμματοσειρές με τις οποίες σχεδιάζετε, δημιουργήστε υποσύνολα για τις μεγάλες, αναζητήστε μια γραμματοσειρά Unicode και RegisterUnicodeTTF τη στιγμή που το κείμενο εγκαταλείπει τα Λατινικά, και διατηρήστε έναν σωστό χάρτη ToUnicode εάν κάποιος πρόκειται να εξάγει το κείμενο. Κάντε αυτά σωστά και τα κουτιά θα σταματήσουν να εμφανίζονται. Για τους γύρω μηχανισμούς, η ανατομία ενός ελάχιστου PDF δείχνει πού βρίσκεται το λεξικό της γραμματοσειράς στο δέντρο αντικειμένων, και η επισκόπηση της δομής του εγγράφου καλύπτει τον τρόπο με τον οποίο οι πόροι διαμοιράζονται στις σελίδες

Οι κλήσεις SetFont, FontEmbedding και RegisterUnicodeTTF που εμφανίζονται εδώ αποτελούν μέρος του HotPDF Component για Delphi και C++Builder