Οι shaped γλυφές αποδίδονται ως κουτιά .notdef όταν ένας font subsetter κρατά μόνο τις γλυφές που είναι προσβάσιμες από τα εκπεμπόμενα code points. Το HotPDF, το native VCL PDF component για Delphi και C++Builder, έφερε ακριβώς αυτό το ελάττωμα μέχρι την έκδοση 2.435.0: η έξοδος OpenType GSUB καταγραφόταν σε ένα εσωτερικό bitmap χρήσης που ο subsetter δήλωνε ότι θα τιμούσε και μετά ποτέ δεν διάβαζε στην πραγματικότητα
Αυτή είναι μια διαφορετική αποτυχία από αυτήν που περιγράφεται στο σφάλμα EndDoc που απενεργοποιούσε σιωπηλά το font subsetting. Εκείνο το σφάλμα αφορούσε πότε έτρεχε το subsetting σε σχέση με τη σειριοποίηση, και απενεργοποιούσε το subsetting εξ ολοκλήρου. Αυτό αφορά τι περιέχει το subset όταν το subsetting τρέχει τέλεια στο πρόγραμμά του. Το pipeline πυροδοτείται τη σωστή στιγμή, το πρόθεμα subset έξι γραμμάτων εμφανίζεται στο /BaseFont ακριβώς όπως απαιτεί το ISO 32000-1 §9.6.4, το αρχείο γίνεται μικρότερο, κάθε λατινική σελίδα ελέγχεται καθαρή, και μια αραβική σελίδα βγαίνει ως μια σειρά από κενά ορθογώνια. Τα σφάλματα σειράς είναι θορυβώδη μόλις τα κοιτάξει κανείς. Τα σφάλματα closure παραμένουν σιωπηλά για πάντα, επειδή το subset είναι δομικά έγκυρο και λάθος μόνο ως προς τη δική του λίστα μελών
Γιατί οι shaped γλυφές αποδίδονται ως .notdef;
Επειδή το σύνολο των code points που εκπέμπει ένα έγγραφο δεν είναι το σύνολο των γλυφών που σχεδιάζει το έγγραφο, και ένας subsetter που τα συγχέει τα δύο ρίχνει κάθε γλυφή που παράγεται από shaping. Το text shaping μετατρέπει μια λογική ακολουθία χαρακτήρων σε μια τοποθετημένη ακολουθία γλυφών, και ολόκληρος ο σκοπός του είναι να παράγει γλυφές που κανένας μεμονωμένος χαρακτήρας εισόδου δεν αντιστοιχίζει: ένα αραβικό μεσαίο heh, μια συνδεδεμένη γραφή fi, ένα σύμπλεγμα Devanagari, μια εναλλακτική με βάση συμφραζόμενα που επιλέγεται από το χαρακτηριστικό rclt. Καθεμία από αυτές είναι ένα glyph ID που κατασκεύασε ένα GSUB lookup, όχι ένα που σας δίνει ο πίνακας cmap για οποιονδήποτε χαρακτήρα στη συμβολοσειρά σας. Ένας subsetter που οδηγείται αποκλειστικά από το cmap επομένως διατρέχει το λάθος ευρετήριο. Διατηρεί πιστά κάθε γλυφή που το κείμενο θα μπορούσε να έχει χρησιμοποιήσει πριν το shaping και απορρίπτει ακριβώς τις γλυφές που το κείμενο πράγματι χρησιμοποιεί μετά το shaping. Ο renderer τότε ζητά από την ενσωματωμένη γραμματοσειρά το GID 1847, το subset έχει μηδενίσει εκείνη την εγγραφή στο loca, και επιστρέφει αντί αυτού ο δείκτης γλυφής 0. Ο δείκτης γλυφής 0 είναι .notdef εξ ορισμού στο OpenType, για αυτό η υπογραφή της αποτυχίας είναι ένα κενό κουτί αντί για λάθος γράμμα ή κατάρρευση. Τίποτα στο PDF δεν είναι κακοσχηματισμένο· η γραμματοσειρά απλώς δεν περιέχει τη γλυφή που ζήτησε το content stream
Τα code points δεν είναι glyphs: οι τρεις πηγές ενός subset
Ένα σωστό subset closure πρέπει να ενώνει τρεις ανεξάρτητες πηγές, καθεμία με τον δικό της συσσωρευτή. Η πρώτη είναι το σύνολο που προκύπτει από code points: το HotPDF συσσωρεύει το FUnicodeUsedCps καθώς εκπέμπονται χαρακτήρες BMP και το FUnicodeSmpUsed για χαρακτήρες συμπληρωματικού επιπέδου που προσεγγίζονται μέσω surrogate pairs, μετά αντιστοιχίζει τον καθένα μέσω του FUnicodeCpToGid σε ένα glyph ID. Η δεύτερη είναι το σύνολο που προκύπτει από shaping, τα glyph IDs που παρήγαγε μια αντικατάσταση GSUB, καταγεγραμμένα μέσω των MarkUnicodeGlyphUsed και EnableShapingFeatureForSubset στο FUnicodeExtraUsedGlyphs. Η τρίτη είναι το composite closure: μια γλυφή της οποίας το numberOfContours είναι -1 στο glyf συναρμολογείται από glyph IDs συστατικών, και η διατήρηση του composite ενώ ρίχνονται τα συστατικά του αποδίδει ένα κενό περίγραμμα αντί για .notdef, κάτι που είναι μάλλον χειρότερο επειδή διαβάζεται ως σφάλμα διαστήματος
Το HotPDF χειριζόταν πάντα την πρώτη και την τρίτη. Η BuildAndApplyUnicodeFontSubset, το σημείο εισόδου του subsetting που καλεί το EndDoc πριν τη σειριοποίηση, σπέρνει τον πίνακα χρησιμοποιημένων γλυφών με GID 0, διατρέχει τα code points BMP, διατρέχει τη λίστα χρήσης SMP, και παραδίδει τον πίνακα σε έναν subset builder που επιλύει τα συστατικά composite εσωτερικά. Η δεύτερη πηγή γραφόταν αλλά ποτέ δεν καταναλωνόταν, και επειδή οι τρεις πηγές αποτυγχάνουν σε διαφορετικό περιεχόμενο, το κενό μπορεί να κρύβεται για χρόνια σε μια codebase της οποίας το corpus παλινδρόμησης είναι κυρίως λατινικό
Ο πίνακας που γραφόταν και ποτέ δεν διαβαζόταν
Το συμβόλαιο ήταν τεκμηριωμένο σε τρία σημεία και τηρημένο σε κανένα από αυτά. Η δήλωση του FUnicodeExtraUsedGlyphs δήλωνε ότι ο subsetter του EndDoc το ενώνει με τη χρήση που προκύπτει από code points· το σχόλιο κεφαλίδας στην ApplyArabicGSUBRefinement υποσχόταν ότι κάθε εκπεμπόμενο υποκατάστατο GID περνά μέσα από την MarkUnicodeGlyphUsed ώστε ο subsetter να τραβήξει τη γλυφή μέσα στην ενσωματωμένη γραμματοσειρά· η ίδια υπόσχεση εμφανίζεται αυτολεξεί στην ApplyArabicGSUBContextualRefinement για τη διαδρομή rclt. Και οι δύο καλούντες τήρησαν το δικό τους μισό. Ένα grep πάνω σε κάθε αναφορά στο πεδίο διευθέτησε το άλλο μισό σε περίπου ενενήντα δευτερόλεπτα: μία δήλωση, μία κατανομή SetLength μέσα στην RegisterUnicodeTTF, και εγγραφές στις δύο ρουτίνες σημαδέματος. Ούτε μία ανάγνωση. Αυτό είναι το διαγνωστικό που αξίζει να αφομοιωθεί, επειδή γενικεύεται πολύ πέρα από γραμματοσειρές. Όταν ένα πεδίο γράφεται από πολλά σημεία κλήσης και δεν διαβάζεται από κανένα, το χαρακτηριστικό που αντιπροσωπεύει δεν υπάρχει, όσο διεξοδικά και αν είναι σχολιασμένο. Το Βήμα 1 του subsetter είναι αρκετά μικρό για να διαβαστεί σε μία οθόνη, και το κενό είναι προφανές μόλις ξέρει κανείς να το αναζητήσει
// Step 1: derive the used-glyph set (as it stood before 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
UsedGlyphs[I] := False;
UsedGlyphs[0] := True; // .notdef is always present
for Cp := 0 to $FFFF do // source 1a: BMP code points
if (Cp < Length(FUnicodeUsedCps)) and FUnicodeUsedCps[Cp]
and (Cp < Length(FUnicodeCpToGid)) then
begin
GID := FUnicodeCpToGid[Cp];
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
for I := 0 to High(FUnicodeSmpUsed) do // source 1b: SMP code points
begin
GID := FUnicodeSmpUsed[I].GID;
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
// source 2 was missing here: nothing ever consulted FUnicodeExtraUsedGlyphs
Η διόρθωση ενός βρόχου, και το σημάδεμα γλυφών από εσάς
Η επιδιόρθωση είναι μια ένωση, και το επιχείρημα ασφάλειάς της προέρχεται από την κατεύθυνση της πράξης: μόνο θέτει bits, ποτέ δεν τα καθαρίζει, οπότε καμία γλυφή που παλιά επιζούσε στο subset δεν μπορεί να αρχίσει να ρίχνεται
// v2.435.0: pull GSUB-derived extra glyphs into the subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset record GIDs that
// shaping produced but that no emitted code point maps to directly.
for I := 0 to FUnicodeNumGlyphs - 1 do
if (I < Length(FUnicodeExtraUsedGlyphs)) and FUnicodeExtraUsedGlyphs[I] then
UsedGlyphs[I] := True;
Τρεις ιδιότητες κάνουν αυτό μια αλλαγή χαμηλού κινδύνου αντί για επανεγγραφή του font-engine. Είναι μονότονη, όπως παραπάνω. Είναι no-op σε γραμματοσειρές που ποτέ δεν έκαναν shaping σε τίποτα, αφού το FUnicodeExtraUsedGlyphs παραμένει όλο-False και η έξοδος bytes για ένα έγγραφο μόνο-λατινικό παραμένει αμετάβλητη. Και προσγειώνεται πριν το Βήμα 2, οπότε και οι δύο subset builders το κληρονομούν: ο αραιός builder που διατηρεί την αρχική αρίθμηση GID, και ο συμπαγής builder _BuildCompactSubsetTTF που το HotPDF επιλέγει υπό PDF/A για να επαναριθμήσει τις κρατημένες γλυφές σε ένα πυκνό εύρος, να συρρικνώσει το maxp.numGlyphs, και να εκπέμψει την αντιστοίχιση παλιού-σε-νέο ως το stream /CIDToGIDMap που απαιτεί το ISO 32000-1 §9.7.4.2. Και οι δύο καλούν εσωτερικά την _TTFWalkCompositeClosure, οπότε μια shaped γλυφή που τυχαίνει να είναι composite τώρα παρασύρει μαζί της και τα συστατικά της. Το composite closure ποτέ δεν ήταν σπασμένο· απλώς ποτέ δεν προσεγγιζόταν για αυτά τα glyph IDs, επειδή τα glyph IDs δεν βρίσκονταν στο σύνολο που διατρέχει. Αν κάποιος οδηγεί απευθείας τη μηχανή GSUB αντί να βασίζεται στα ενσωματωμένα περάσματα βελτίωσης, το closure γίνεται δική του ευθύνη, και κάθε glyph ID υποκατάστασης που εκπέμπεται πρέπει να σημαδεύεται πριν το EndDoc παγώσει το σύνολο χρησιμοποιημένων γλυφών
var
Pdf: THotPDF;
GIDs: array[0..1] of Word;
LigGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'shaped.pdf';
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoNaskhArabic-Regular.ttf');
Pdf.ShapingFeatures := [sfArabicGSUB, sfStandardLigatures,
sfContextualAlternates];
GIDs[0] := Pdf.GetUnicodeGlyphForCodepoint($0644); // lam
GIDs[1] := Pdf.GetUnicodeGlyphForCodepoint($0627); // alef
if Pdf.ApplyLigatureSubstitution(GIDs, 0, 'liga', LigGID) then
Pdf.MarkUnicodeGlyphUsed(LigGID); // omit this and you get .notdef
Pdf.EnableShapingFeatureForSubset('rclt');
Pdf.CurrentPage.RtLTextOut(50, 700, 0, WideString(ArabicText));
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Η EnableShapingFeatureForSubset είναι το batch αντίστοιχο της κλήσης ενός μεμονωμένου GID, και είναι σκόπιμα συντηρητική. Διατρέχει τη λίστα lookup GSUB για τα lookups συνδεδεμένα με μια ετικέτα χαρακτηριστικού τεσσάρων bytes κάτω από το τρέχον επιλεγμένο script και τη διαδρομή γλώσσας, και σημαδεύει τα glyph IDs υποκατάστασης που μπορούν να παράγουν εκείνα τα lookups. Είναι ένα αμυντικό no-op όταν η γραμματοσειρά δεν φέρει πίνακα GSUB ή όταν το χαρακτηριστικό απουσιάζει από εκείνη τη διαδρομή, οπότε η κλήση της ανεπιφύλακτα είναι ασφαλής. Είναι επίσης μια υπερεκτίμηση εκ σχεδιασμού: μπορεί να διατηρήσει γλυφές που ένα συγκεκριμένο έγγραφο ποτέ δεν σχεδιάζει. Για το subsetting, η υπερ-συμπερίληψη κοστίζει bytes και η υπο-συμπερίληψη κοστίζει ορθότητα, κάτι που κάνει εκείνο το αντάλλαγμα εύκολο. Η δομή αυτών των lookups, και οι πίνακες κάλυψης που αποφασίζουν ποιες γλυφές συμμετέχουν, καλύπτονται στην επισκόπηση των στυλιστικών εναλλακτικών GSUB σε καθαρή Delphi
Πώς αποδεικνύεται ότι η γλυφή βρίσκεται πράγματι στο subset;
Με την ανάγνωση της εκπεμπόμενης γραμματοσειράς, όχι με οπτικό έλεγχο της σελίδας σε έναν viewer που μπορεί να υποκαθιστά μια γραμματοσειρά συστήματος πίσω από την πλάτη κάποιου. Ο έλεγχος που πιάνει ολόκληρη αυτή την κατηγορία σφάλματος είναι μηχανικός: εξαγωγή του stream /FontFile2 από το PDF εξόδου, ανάλυση του loca, και επιβεβαίωση ότι το glyph ID που αναμένεται φέρει μη κενή εγγραφή, δηλαδή τα offsets αρχής και τέλους του διαφέρουν. Μια κενή εγγραφή είναι ο subsetter να έχει αποφασίσει ότι η γλυφή δεν χρησιμοποιείται. Δύο συνήθειες κάνουν τότε πολύ πιο δύσκολο να κυκλοφορήσει ξανά η αποτυχία. Διατήρηση μιας σελίδας shaped-script στο αυτοματοποιημένο corpus smoke αντί μόνο στο χειροκίνητο σύνολο ελέγχου, επειδή τα Αραβικά, τα Devanagari, και τα Khmer ασκούν διαδρομές closure που καμία ποσότητα κάλυψης λατινικών δεν θα αγγίξει. Και όποτε υπάρχει ένας συσσωρευτής, να επιβεβαιώνεται ότι κάτι τον καταναλώνει, αφού ένα πεδίο μόνο-για-εγγραφή είναι ένα χαρακτηριστικό που μεταγλωττίζεται, περνά τα tests σε λάθος corpus, και δεν κάνει τίποτα
Πού σταματά η διόρθωση
Το subset closure είναι αναγκαίο για να αποδοθεί μια shaped γλυφή, αλλά δεν είναι επαρκές. Η γλυφή πρέπει επίσης να είναι διευθυνσιοδοτήσιμη από το content stream, που είναι ξεχωριστό πρόβλημα με το δικό του όριο. Τα ενσωματωμένα περάσματα βελτίωσης Αραβικών του HotPDF δεσμεύουν μια αντικατάσταση μόνο όταν κάθε glyph ID υποκατάστασης είναι προσβάσιμο μέσω ενός code point μορφής παρουσίασης Unicode μέσω μιας αντίστροφης σάρωσης cmap πάνω σε περίπου 690 code points στα U+FB50 έως U+FDFF και U+FE70 έως U+FEFF. Όταν μια υποκατάσταση προσγειώνεται σε ένα glyph ID εκτός εκείνου του εύρους, το παράθυρο εισόδου περνά αμετάβλητο αντί να εκπέμπει κάτι που ο αναγνώστης δεν μπορεί να διευθυνσιοδοτήσει· εναλλακτικές ειδικές για τη γραμματοσειρά σε αυθαίρετα glyph IDs χρειάζονται ένα συνθετικό code point ιδιωτικής χρήσης δεσμευμένο στα U+E000 έως U+F8FF για να τις μεταφέρουν μέσα από τη διαδρομή εκπομπής. Έτσι η ειλικρινής περίληψη είναι ότι η διόρθωση 2.435.0 αφαίρεσε έναν σκληρό αποκλειστή αντί να ολοκληρώσει την ιστορία. Πριν από αυτήν, μια γλυφή μπορούσε να έχει σχηματιστεί σωστά, να εκπεμφθεί σωστά, και ακόμα να εξαφανιστεί τη στιγμή του subset, κάτι που σήμαινε ότι η μηχανή shaping δεν μπορούσε να είναι έμπιστη από άκρη σε άκρη όσο καλά και αν ήταν τα lookups της. Αυτό που απομένει είναι η διευθυνσιοδότηση, και εκείνος ο περιορισμός τουλάχιστον αποτυγχάνει ορατά στο σημείο εκπομπής αντί σιωπηλά σε ένα βήμα build που τρέχει μετά από όλα όσα παρακολουθούσε κανείς. Για την πλευρά εκπομπής του ίδιου pipeline, βλέπε τον οδηγό για shaping αραβικού και RTL κειμένου σε PDF Delphi
Το font subsetting, η μηχανή GSUB, και το shaping πολύπλοκων scripts που περιγράφονται εδώ διατίθενται στο τυπικό HotPDF Component για Delphi και C++Builder· η σελίδα προϊόντος φέρει την πλήρη αναφορά API για τις κλήσεις γραμματοσειράς Unicode και shaping που αναφέρθηκαν παραπάνω