Δημιουργήστε μια αναφορά, ενσωματώστε μια γραμματοσειρά TrueType και το αρχείο εξόδου ανοίγει σωστά σε κάθε πρόγραμμα προβολής που δοκιμάζετε. Οι γλύφοι είναι σωστοί, το κείμενο μπορεί να επιλεγεί, το αρχείο είναι έγκυρο. Το μόνο πρόβλημα είναι το μέγεθος. Ένα έγγραφο που χρησιμοποίησε μερικές δεκάδες λατινικούς χαρακτήρες μεταφέρει ολόκληρη τη γραμματοσειρά των 350 KB. Ένα έγγραφο που εκτύπωσε μια παράγραφο στα κινεζικά μεταφέρει μια CJK γραμματοσειρά 14 MB αντί για το τμήμα του μισού megabyte που θα έπρεπε να χρειάζεται. Δεν παρουσιάστηκε καμία εξαίρεση, δεν καταγράφηκε καμία προειδοποίηση και το αρχείο πέρασε την επικύρωση. Έτσι φαίνεται εξωτερικά ένα βήμα οριστικοποίησης με λανθασμένη σειρά: τίποτα δεν αποτυγχάνει και η μόνη απόδειξη είναι ένας αριθμός που είναι πολύ μεγάλος.
Το σφάλμα που το προκάλεσε υπήρχε στο HotPDF για μία σειρά εκδόσεων και έκτοτε έχει διορθωθεί. Αξίζει να αναφερθεί όχι ως ειδοποίηση ελαττώματος αλλά ως μάθημα, επειδή η μορφή του λάθους είναι γενική. Κάθε μηχανή εγγράφων έχει ένα στάδιο οριστικοποίησης που τροποποιεί τα αντικείμενα ακριβώς πριν τα γράψει, και η ορθότητα αυτού του σταδίου εξαρτάται εξ ολοκλήρου από τη σειρά των βημάτων του σε σχέση με τη σειριοποίηση (serialization). Αν τοποθετήσετε ένα βήμα στη λάθος πλευρά της εγγραφής, αυτό δεν κάνει τίποτα, αθόρυβα.
Τι υποτίθεται ότι κάνει το font subsetting
Μια υποσύνολο γραμματοσειρά (subset font) είναι το μέρος ενός αρχείου TrueType που χρησιμοποιεί στην πραγματικότητα ένα έγγραφο. Το πρότυπο ISO 32000-1 §9.9 περιγράφει πώς ένα ενσωματωμένο πρόγραμμα γραμματοσειράς βρίσκεται σε μια ροή που αναφέρεται από τον περιγραφέα γραμματοσειράς (font descriptor), και για ένα πρόγραμμα TrueType αυτή η ροή είναι η /FontFile2 με ένα /Length1 που δίνει τον μη συμπιεσμένο αριθμό byte. Το subsetting ξαναγράφει τους πίνακες glyf και loca ώστε να περιέχουν μόνο τους γλύφους που αναφέρει το έγγραφο, επαναριθμεί τα αναγνωριστικά γλύφων και προσθέτει στο όνομα /BaseFont ένα πρόθεμα έξι γραμμάτων όπως το ABCDEF+ για να επισημάνει τη γραμματοσειρά ως υποσύνολο, ακριβώς όπως απαιτεί η προδιαγραφή. Μια λατινική γραμματοσειρά που περιορίζεται σε δέκα ή δεκαπέντε kilobyte είναι η διαφορά μεταξύ ενός ελαφριού PDF και ενός που μεταφέρει μια ολόκληρη γραμματοσειρά για χάρη μιας μόνο επικεφαλίδας.
Το σημείο στο οποίο συμβαίνει αυτό έχει σημασία. Το subsetting δεν είναι ένας μετασχηματισμός που εφαρμόζετε σε byte που βρίσκονται ήδη στο δίσκο. Επεξεργάζεται το γράφημα αντικειμένων στη μνήμη: συρρικνώνει το περιεχόμενο της ροής /FontFile2, διορθώνει το /Length1 και ξαναγράφει τη συμβολοσειρά /BaseFont. Όλα αυτά πρέπει να είναι στη θέση τους όταν ο σειριοποιητής (serializer) διασχίζει το γράφημα και εκπέμπει byte. Αν οι επεξεργασίες γίνουν αφού γραφτούν τα byte, ενημερώνουν αντικείμενα που κανείς δεν θα διαβάσει ποτέ.
Το σύμπτωμα και γιατί δεν υπήρξε καμία αναφορά σφάλματος
Η αναφερόμενη συμπεριφορά ήταν η παρουσία πλήρων γραμματοσειρών στο αρχείο εξόδου χωρίς κανένα διαγνωστικό μήνυμα. Ένας χρήστης που καταχώρισε μια γραμματοσειρά Unicode TrueType και παρήγαγε ένα κανονικό έγγραφο διαπίστωσε ότι το ενσωματωμένο αντικείμενο γραμματοσειράς είχε το ίδιο μήκος με το αρχείο προέλευσης .ttf και ότι το όνομα /BaseFont δεν έφερε το εξαγράμματο πρόθεμα υποσυνόλου. Το αρχείο εξόδου δεν συρρικνωνόταν ποτέ μεταξύ εκτελέσεων που χρησιμοποιούσαν δέκα γλύφους και εκτελέσεων που χρησιμοποιούσαν δέκα χιλιάδες.
Η απουσία οποιουδήποτε σφάλματος είναι το στοιχείο που καθιστά αυτή την κατηγορία σφαλμάτων δαπανηρή. Μια ρουτίνα δημιουργίας υποσυνόλου που εκτελείται σε λάθος χρόνο εξακολουθεί να εκτελείται. Διασχίζει τη συσσωρευμένη χρήση codepoints, δημιουργεί ένα απόλυτα σωστό υποσύνολο και το εφαρμόζει στο γράφημα αντικειμένων στη μνήμη. Εσωτερικά η εργασία ολοκληρώνεται και η κλήση επιστρέφει καθαρά. Το μόνο λάθος είναι ότι το γράφημα αντικειμένων που επεξεργάστηκε δεν είναι πλέον αυτό που γράφεται, επειδή ο συγγραφέας (writer) έχει ήδη τελειώσει. Από την πλευρά του καλούντος, το έγγραφο παρήχθη και αποθηκεύτηκε χωρίς προβλήματα, κάτι που είναι ακριβώς η εντύπωση που δίνει μια σιωπηλή αποτυχία.
Η βασική αιτία ήταν η σειρά οριστικοποίησης
Στο HotPDF η εργασία κλεισίματος πραγματοποιείται μέσα στο EndDoc. Το βήμα δημιουργίας υποσυνόλου είναι μια εσωτερική ρουτίνα με το όνομα BuildAndApplyUnicodeFontSubset. Διαβάζει το σύνολο των χρησιμοποιούμενων codepoints ανά έγγραφο, το οποίο διατηρείται σε ένα bitmap που γεμίζει η διαδρομή εκπομπής κειμένου καθώς εμφανίζονται οι γλύφους, αντιστοιχίζει κάθε χρησιμοποιούμενο codepoint μέσω του προσωρινά αποθηκευμένου πίνακα codepoint-to-glyph σε ένα πραγματικό αναγνωριστικό γλύφου και ξαναγράφει το πρόγραμμα γραμματοσειράς γύρω από αυτό το κλείσιμο. Όταν καταχωρείται μια γραμματοσειρά Unicode TrueType, η διαδρομή εκπομπής ορίζει ένα bit στο σύνολο χρησιμοποιούμενων codepoints ε για κάθε χαρακτήρα που σχεδιάζει, οπότε μέχρι να κλείσει το έγγραφο, η μηχανή γνωρίζει ακριβώς ποιους γλύφους πρέπει να διατηρήσει το υποσύνολο.
Το ελάττωμα ήταν ότι η BuildAndApplyUnicodeFontSubset καλούνταν αφού η SaveToStream ή η SaveToFile είχε ήδη σειριοποιήσει το έγγραφο. Οι επεξεργασίες του δημιουργού υποσυνόλου στη ροή /FontFile2, το διορθωμένο /Length1 και το εξαγράμματο πρόθεμα /BaseFont υπολογίστηκαν όλα πάνω σε ένα γράφημα αντικειμένων που είχε ήδη μετατραπεί σε byte. Η διόρθωση ήταν μια αναδιάταξη μιας γραμμής: μετακίνηση της κλήσης υποσυνόλου πριν από τη σειριοποίηση, ώστε ο συγγραφέας να εκπέμπει την υποσύνολο γραμματοσειρά αντί για την αρχική. Η διορθωμένη αλληλουχία εκτελεί πρώτα τη δημιουργία υποσυνόλου και στη συνέχεια τη σειριοποίηση.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Με τη σειρά διορθωμένη, τίποτα δεν αλλάζει στον κώδικα κλήσης. Η δημιουργία υποσυνόλου είναι ενεργοποιημένη από προεπιλογή μόλις καταχωριστεί μια γραμματοσειρά Unicode TrueType. Καταχωρείτε τη γραμματοσειρά, ξεκινάτε το έγγραφο, σχεδιάζετε και το τερματίζετε, και το υποσύνολο δημιουργείται από τους γλύφους που χρησιμοποιήσατε πριν τα byte φύγουν από τη μνήμη.
Γιατί ένα λάθος τοποθετημένο βήμα αποτελεί ολόκληρη κατηγορία
Ο λόγος που αυτό αξίζει ένα μάθημα αντί για μια υποσημείωση είναι ότι η EndDoc εκπέμπει μια λίστα βημάτων κλεισίματος, και κάθε ένα από αυτά είναι ευαίσθητο στη θέση του σε σχέση με την εγγραφή. Το font subsetting είναι ένα από αυτά. Η έξοδος PDF/A απαιτεί μια ροή /CIDSet που απαριθμεί ακριβώς τα αναγνωριστικά γλύφων που υπάρχουν στο υποσύνολο, ένας περιορισμός που επιβάλλει το πρότυπο ISO 19005 ώστε ένας επικυρωτής να μπορεί να επιβεβαιώσει ότι το ενσωματωμένο πρόγραμμα ταιριάζει με αυτό που ισχυρίζεται ο περιγραφέας γραμματοσειράς. Αυτή η ροή εκπέμπεται στο ίδιο παράθυρο οριστικοποίησης και εξαρτάται από το αν το υποσύνολο έχει δημιουργηθεί πρώτα. Το πρότυπο PDF/UA-1 απαιτεί, σύμφωνα με το ISO 14289-1 §7.18.3, κάθε σελίδα που φέρει σχολιασμό να δηλώνει την /Tabs με την τιμή /S, και μια εσωτερική ρουτίνα με το όνομα EnsurePDFUATabsOnAnnotatedPages εισάγει αυτό το κλειδί κατά το ίδιο στάδιο. Οι έλεγχοι πρόθεσης εξόδου (output-intent) εκτελούνται επίσης εκεί.
Το ίδιο σφάλμα σειράς που απενεργοποίησε το subsetting παρέλειψε επίσης το κλειδί σειράς καρτελών PDF/UA στις σελίδες με σχόλια, επειδή αυτό το βήμα βρισκόταν στην ίδια λάθος πλευρά της εγγραφής. Τα προγράμματα veraPDF και PAC αναφέρουν ένα ελλιπές /Tabs /S ως παραβίαση του σημείου ελέγχου 21-001 του πρωτοκόλλου Matterhorn. Έτσι, μια μεμονωμένη λανθασμένη κλήση δεν αύξησε απλώς το μέγεθος του αρχείου, αλλά ταυτόχρονα παραβίασε σιωπηλά μια απαίτηση συμμόρφωσης προσβασιμότητας, με την ίδια έλλειψη οποιουδήποτε σφάλματος. Αυτός είναι ο κίνδυνος ενός σταδίου οριστικοποίησης: τα βήματά του μοιράζονται μια προϋπόθεση, και ένα μόνο λάθος σειράς μπορεί να θέσει εκτός λειτουργίας αρκετά από αυτά ταυτόχρονα, ενώ κάθε κλήση εξακολουθεί να επιστρέφει επιτυχία.
Πώς εντοπίζεται στην πραγματικότητα μια σιωπηλή αποτυχία εκπομπής
Ένα σφάλμα που δεν προκαλεί εξαίρεση δεν εντοπίζεται με την εκτέλεση του προγράμματος. Εντοπίζεται με την επιθεώρηση της εξόδου και τη σύγκρισή της με αυτό που θα έπρεπε να είχε παραγάγει η είσοδος. Για το font subsetting οι έλεγχοι είναι συγκεκριμένοι. Συγκρίνετε το μέγεθος του αρχείου εξόδου με μια γενική προσδοκία: ένα έγγραφο που χρησιμοποίησε ελάχιστους γλύφους δεν πρέπει να έχει το μέγεθος μιας πλήρους γραμματοσειράς. Ανοίξτε το ενσωματωμένο αντικείμενο γραμματοσειράς και διαβάστε το μήκος του σε byte. Μια ροή /FontFile2 με υποσύνολο για μια λατινική γραμματοσειρά αποτελεί ένα μικρό κλάσμα του αρχείου προέλευσης. Διαβάστε το όνομα /BaseFont και επιβεβαιώστε ότι το εξαγράμματο πρόθεμα είναι παρόν, καθώς η απουσία του είναι ένα άμεσο μήνυμα ότι δεν εφαρμόστηκε κανένα υποσύνολο.
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Για την έξοδο PDF/A ο έλεγχος είναι ακόμη πιο αυστηρός, επειδή ένας επικυρωτής κάνει τη δουλειά για εσάς. Ορίστε το επίπεδο συμμόρφωσης και περάστε το αποτέλεσμα από το veraPDF: ένα ελλιπές /CIDSet, ή ένα υποσύνολο που δεν ταιριάζει με τον περιγραφέα, αναφέρεται ως αποτυχημένη ρήτρα αντί να αφεθεί να το παρατηρήσετε με το μάτι. Οι διακόπτες συμμόρφωσης που καθοδηγούν αυτή την εργασία οριστικοποίησης είναι ιδιότητες στο έγγραφο. Η ιδιότητα PDFACompliance δέχεται μια συμβολοσειρά όπως '2B' για PDF/A-2 Level B, και η PDFUACompliance είναι μια τιμή boolean που ενεργοποιεί τις απαιτήσεις tagged-PDF και σειράς καρτελών.
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
Το μάθημα μηχανικής
Δύο κανόνες προκύπτουν από αυτό. Ο πρώτος είναι ότι οποιοδήποτε βήμα οριστικοποίησης που τροποποιεί αντικείμενα πρέπει να εκτελείται πριν από τη σειριοποίηση αυτών των αντικειμένων, και το τελικό στάδιο μιας μηχανής εγγράφων θα πρέπει να αντιμετωπίζεται ως μια διατεταγμένη διοχέτευση (pipeline) όπου η σειριοποίηση είναι η τελευταία ενέργεια, όχι μία ενέργεια μεταξύ πολλών. Ο δεύτερος είναι αυτός που κόστισε τον περισσότερο χρόνο εδώ: για ένα βήμα εκπομπής, η απουσία σφάλματος δεν αποτελεί απόδειξη επιτυχίας. Μια ρουτίνα που δημιουργεί το σωστό υποσύνολο και το εφαρμόζει στο λάθος, ήδη γραμμένο γράφημα δεν αναφέρει τίποτα λάθος, επειδή από τη δική της οπτική γωνία δεν υπήρχε κανένα πρόβλημα. Η επαλήθευση πρέπει να εξετάζει το παραγόμενο αρχείο, όχι τον κωδικό επιστροφής. Ελέγξτε το μέγεθος εξόδου, διαβάστε το μήκος σε byte της ενσωματωμένης γραμματοσειράς και το πρόθεμα της /BaseFont, και αφήστε το veraPDF να κρίνει την έξοδο PDF/A όπου ένα ελλιπές /CIDSet μετατρέπει μια σιωπηλή έλλειψη σε επώνυμη αποτυχία.
Η πλευρά παραγωγής του χειρισμού γραμματοσειρών, ο τρόπος καταχώρισης και ενσωμάτωσης των γραμματοσειρών για έξοδο αναφοράς, καλύπτεται στο άρθρο μας σχετικά με τις γραμματοσειρές και τις εικόνες στην έξοδο αναφοράς. Η πλευρά της επικύρωσης, όπου αυτά τα βήματα οριστικοποίησης ελέγχονται έναντι των προτύπων, καλύπτεται στον οδηγό για την επικύρωση PDF/A και PDF/UA. Και τα δύο συνδυάζονται με τις εργασίες υποσυνόλων και συμμόρφωσης που περιγράφονται εδώ, οι οποίες παρέχονται ως μέρος του HotPDF Component για Delphi και C++Builder μαζί με τα API φόρτωσης, επεξεργασίας, κρυπτογράφησης και υπογραφής που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου.