Technical Article

OpenType GSUB Stylistic Alternates σε καθαρό Delphi

Ένας σχεδιαστής επιλέγει μια γραμματοσειρά με ένα a μονής ιστορίας για επικεφαλίδες, ή ένα slashed zero για πίνακες, ή ένα σύνολο swash κεφαλαίων για ένα εξώφυλλο. Αυτά τα γλυφικά (glyphs) βρίσκονται ήδη στη γραμματοσειρά. Απλώς δεν είναι τα προεπιλεγμένα. Το προεπιλεγμένο a αντιστοιχίζεται από τον χαρακτήρα μέσω του πίνακα cmap σε ένα γλυφικό, και το εναλλακτικό βρίσκεται λίγα glyph ids μακριά, προσβάσιμο μόνο μέσω ενός κανόνα αντικατάστασης (substitution rule). Η παραγωγή αυτού του εναλλακτικού σε ένα PDF σημαίνει ανάγνωση του κανόνα και εκπομπή του υποκατάστατου γλυφικού στη ροή περιεχομένου (content stream). Αυτό το άρθρο αφορά την ανάγνωση αυτών των κανόνων, του είδους της απλής αντικατάστασης (single-substitution), σε Object Pascal χωρίς εγγενή βιβλιοθήκη διαμόρφωσης (shaping library) από κάτω.

Το πεδίο εφαρμογής είναι σκόπιμα στενό. Τα stylistic sets και τα alternates είναι αντικαταστάσεις της μορφής single-glyph-in, single-glyph-out. Είναι το τμήμα της διάταξης OpenType που μπορείτε να επιλύσετε με μια μικρή, ντετερμινιστική περιήγηση πίνακα, γεγονός που τα καθιστά κατάλληλα για μια μηχανή Pascal που θέλει να παραμείνει ελεύθερη από εξαρτήσεις C.

Γιατί καθαρό Delphi αντί για HarfBuzz

Το HarfBuzz είναι η προφανής απάντηση στο "διαμόρφωσε αυτό το κείμενο", και για πλήρη αμφίδρομη, Indic ή αραβική διαμόρφωση είναι η σωστή απάντηση. Είναι επίσης μια βιβλιοθήκη C. Η σύνδεσή της σε ένα προϊόν Delphi ή C++Builder σημαίνει την αποστολή ενός εγγενούς αντικειμένου για κάθε πλατφόρμα στόχο και αρχιτεκτονική, την αντιστοίχιση της σύμβασης κλήσης της, την παρακολούθηση του ρυθμού εκδόσεών της και την ανάγνωση των όρων άδειας χρήσης της έναντι της δικής σας. Τίποτα από αυτά δεν είναι δύσκολο μεμονωμένα. Όλα αυτά όμως είναι τριβή που δεν εξαφανίζεται ποτέ, και δεν προσφέρει τίποτα όταν η πραγματική απαίτηση είναι "δώσε μου τη μορφή ss01 αυτού του γράμματος".

Η απλή αντικατάσταση δεν χρειάζεται μηχανή διαμόρφωσης (shaping engine). Χρειάζεται έναν parser για μερικές μορφές υποπινάκων GSUB και μία ή δύο δυαδικές αναζητήσεις. Η συγγραφή αυτού σε Pascal διατηρεί ολόκληρη την εργαλειοθήκη μέσα σε έναν compiler. Το ειλικρινές όριο είναι ότι αυτή η προσέγγιση χειρίζεται αναζητήσεις αντικατάστασης γλυφικών (glyph substitution lookups) και τίποτα άλλο. Δεν είναι επίλυση bidi, δεν είναι αναδιάταξη Indic και δεν είναι αυτόματη διαμόρφωση βάσει συμφραζομένων (contextual shaping). Όπου αυτά είναι απαραίτητα, είναι απαραίτητα, και ένα ερώτημα απλής αντικατάστασης δεν μπορεί να τα υποκαταστήσει.

Η ιεραρχία GSUB, από πάνω προς τα κάτω

Ο πίνακας Glyph Substitution οργανώνεται ως μια αλυσίδα έμμεσων αναφορών, και ένα ερώτημα αντικατάστασης περπατά την αλυσίδα από την κορυφή. Στην κορυφή βρίσκεται το ScriptList. Μια ετικέτα σεναρίου (script tag) όπως η latn επιλέγει μια καταχώριση, και η ειδική ετικέτα DFLT είναι το προεπιλεγμένο σενάριο που εφαρμόζεται όταν δεν ταιριάζει κανένα πιο συγκεκριμένο σενάριο. Η καταχώριση σεναρίου δείχνει σε ένα LangSys, το σύστημα γλώσσας, με ένα προεπιλεγμένο LangSys για την κοινή περίπτωση και προαιρετικά ονομασμένα για γλώσσες που χρειάζονται διαφορετική συμπεριφορά. Turkish είναι το σύνηθες παράδειγμα, όπου το i με τελεία και το ı χωρίς τελεία απαιτούν τον δικό τους χειρισμό.

Το LangSys ονομάζει ένα σύνολο δεικτών χαρακτηριστικών (feature indices). Κάθε δείκτης δείχνει στο FeatureList, όπου μια εγγραφή χαρακτηριστικού φέρει μια ετικέτα τεσσάρων byte, όπως η ss01, και μια λίστα δεικτών αναζήτησης (lookup indices). Αυτοί οι δείκτες δείχνουν τελικά στο LookupList, όπου ζουν οι πραγματικοί υποπίνακες αντικατάστασης. Έτσι, η επίλυση του ss01 σημαίνει: βρείτε το σενάριο, βρείτε το LangSys του, βρείτε το χαρακτηριστικό του οποίου η ετικέτα είναι ss01, συλλέξτε τις αναζητήσεις που ονομάζει και εφαρμόστε τις. Το HotPDF προεπιλέγει το σενάριο DFLT και το προεπιλεγμένο LangSys, το οποίο είναι αυτό με το οποίο αποστέλλεται η συντριπτική πλειονότητα των λατινικών σχεδίων κειμένου, και εκθέτει έναν τρόπο παράκαμψης της ετικέτας σεναρίου όταν μια γραμματοσειρά συνδέει τα χαρακτηριστικά της κάτω από ένα συγκεκριμένο σενάριο.

Οι πίνακες Coverage αποφασίζουν ποιος συμμετέχει

Κάθε υποπίνακας αντικατάστασης ξεκινά με την ίδια ερώτηση: συμμετέχει αυτό το εισερχόμενο γλυφικό σε αυτόν τον κανόνα και, εάν ναι, πού βρίσκεται στη δική του ευρετηρίαση του κανόνα. Αυτή η ερώτηση απαντάται από έναν πίνακα Coverage, και η απάντηση είναι ένας δείκτης κάλυψης (coverage index), ένας μικρός τακτικός αριθμός που χρησιμοποιεί ο υπόλοιπος υποπίνακας για να αναζητήσει σε τι μετατρέπεται το γλυφικό.

Το Coverage διατίθεται σε δύο μορφές. Η Μορφή 1 είναι μια λίστα με glyph ids ταξινομημένα σε αύξουσα σειρά. Βρίσκετε ένα γλυφικό με μια δυαδική αναζήτηση, και η θέση του στη λίστα είναι ο δείκτης κάλυψής του. Η Μορφή 2 είναι μια λίστα εγγραφών εύρους, καθεμία από τις οποίες περιλαμβάνει ένα αρχικό γλυφικό, ένα τελικό γλυφικό και τον δείκτη κάλυψης στον οποίο αντιστοιχεί το αρχικό γλυφικό. Ένα γλυφικό μέσα σε ένα εύρος λαμβάνει το δείκτη κάλυψής του μετατοπίζοντας τον από την αρχή του εύρους. Η Μορφή 1 είναι συμπαγής όταν τα συμμετέχοντα γλυφικά είναι διάσπαρτα, ενώ η Μορφή 2 όταν εμπίπτουν σε συνεχόμενες σειρές. Και οι δύο είναι ταξινομημένες, επομένως και οι δύο αναζητούνται σε λογαριθμικό χρόνο, και οι δύο επιστρέφουν είτε έναν δείκτη κάλυψης είτε ένα καθαρό "not covered" που επιτρέπει στη μηχανή να αφήσει το γλυφικό ήσυχο.

Απλή αντικατάσταση, οι δύο μορφές

Η απλή αντικατάσταση (Single Substitution) είναι ο LookupType 1, και αντιστοιχίζει ένα γλυφικό σε ακριβώς μία αντικατάσταση. Διαθέτει επίσης δύο μορφές, και ο διαχωρισμός είναι μια βελτιστοποίηση χώρου. Η Μορφή 1 αποθηκεύει μια ενιαία υπογεγραμμένη διαφορά (signed delta). Το εξερχόμενο glyph id είναι το εισερχόμενο glyph id συν αυτή τη διαφορά, modulo 65536. Αυτός είναι ο τρόπος με τον οποίο μια γραμματοσειρά κωδικοποιεί μια αντικατάσταση όπου κάθε συμμετέχον γλυφικό βρίσκεται σε μια σταθερή απόσταση από το εναλλακτικό του, για παράδειγμα ένα μπλοκ lining figures τοποθετημένων σε σταθερή απόσταση από τα αντίστοιχα oldstyle figures. Ο πίνακας Coverage λέει ποια γλυφικά πληρούν τις προϋποθέσεις, και η μοναδική διαφορά εξυπηρετεί όλα αυτά.

Η Μορφή 2 αποθηκεύει έναν ρητό πίνακα από substitute glyph ids. Ο δείκτης κάλυψης από τον πίνακα Coverage είναι ο δείκτης σε αυτόν τον πίνακα, έτσι ώστε το γλυφικό στο δείκτη κάλυψης 0 να γίνεται η πρώτη καταχώριση του πίνακα, ο δείκτης κάλυψης 1 η δεύτερη, και ούτω καθεξής. Η Μορφή 2 χρησιμοποιείται όταν τα εναλλακτικά δεν βρίσκονται σε ομοιόμορφη απόσταση, κάτι που είναι η συνήθης περίπτωση για χειροποίητα stylistic sets. Το ερώτημα είναι το ίδιο από την πλευρά του καλούντος σε κάθε περίπτωση. Πάρτε το εισερχόμενο γλυφικό, περάστε το από το Coverage, και εάν καλύπτεται, εφαρμόστε τη διαφορά ή διαβάστε τη θέση του πίνακα.

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

Η σύμβαση που αξίζει να προσέξετε είναι η απευθείας διέλευση (pass-through). Η GetSingleSubstituteGlyph επιστρέφει το εισερχόμενο glyph id αμετάβλητο σε κάθε αποτυχία: καμία γραμματοσειρά, κανένας πίνακας GSUB, κανένα χαρακτηριστικό που να ταιριάζει, κανένα hit στο Coverage. Αυτό σημαίνει ότι η κλήση είναι ασφαλής να γίνει unconditionally. Ζητάτε το εναλλακτικό, και αν δεν υπάρχει, παίρνετε πίσω ακριβώς αυτό που βάλατε, έτσι ώστε ο κώδικας που καλεί να μην χρειάζεται ποτέ να χειριστεί ως ειδική περίπτωση μια γραμματοσειρά που στερείται του χαρακτηριστικού.

Τι σημαίνουν οι ετικέτες stylistic features

Η ετικέτα χαρακτηριστικού (feature tag) είναι ολόκληρο το λεξιλόγιο του εναλλακτικού που ζητάτε, και οι ετικέτες που σχετίζονται με stylistic εργασίες είναι μια σύντομη λίστα. Το κύριο ζεύγος είναι το salt, stylistic alternates, η γενική πρόσβαση στις εναλλακτικές μορφές ενός γλυφικού, και τα ss01 έως ss20, τα είκοσι αριθμημένα stylistic sets που μπορεί να ορίσει μια γραμματοσειρά, καθένα από τα οποία είναι ένα ονομαστικό πακέτο αντικαταστάσεων που ομαδοποιεί ο σχεδιαστής. Μια γραμματοσειρά μπορεί να τοποθετήσει ένα a μονής ιστορίας και ένα R με ίσιο πόδι κάτω από το ss03, για παράδειγμα, οπότε η ενεργοποίηση αυτού του συγκεκριμένου συνόλου αλλάζει το στυλ και των δύο.

// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

cmap format 12 και τα συμπληρωματικά επίπεδα

Πριν εκτελεστεί οποιαδήποτε αντικατάσταση, ένας χαρακτήρας πρέπει να γίνει γλυφικό, και αυτή είναι η δουλειά του πίνακα cmap. Το ερώτημα αντικατάστασης ξεκινά από ένα glyph id, επομένως η διαδρομή είναι πάντα χαρακτήρας σε γλυφικό μέσω του cmap, και στη συνέχεια γλυφικό σε εναλλακτικό μέσω του GSUB. Το ενδιαφέρον μέρος του cmap είναι η εμβέλειά του. Ένας υποπίνακας μορφής 4 καλύπτει το Basic Multilingual Plane, τα πρώτα 65536 σημεία κώδικα (code points), και αυτό είναι αρκετό για το μεγαλύτερο μέρος του λατινικού κειμένου. Δεν είναι αρκετό για σημεία κώδικα από το U+10000 και πάνω, τα συμπληρωματικά επίπεδα (supplementary planes), όπου ζουν τώρα μαθηματικά αλφαριθμητικά, πολλά σύμβολα και διάφορα ζωντανά σενάρια γραφής.

Η Μορφή 12 είναι ο υποπίνακας που καλύπτει ολόκληρο το εύρος U+0000 έως U+10FFFF. Είναι μια ταξινομημένη λίστα ομάδων, όπου κάθε ομάδα περιλαμβάνει ένα αρχικό σημείο κώδικα, ένα τελικό σημείο κώδικα και ένα αρχικό glyph id, έτσι ώστε μια συνεχόμενη σειρά σημείων κώδικα να αντιστοιχεί σε μια συνεχόμενη σειρά γλυφικών. Το HotPDF επιλύει σημεία κώδικα με μια υβριδική στρατηγική που ταιριάζει με τον τρόπο που διαμορφώνονται τα δεδομένα. Τα σημεία κώδικα στο BMP εξυπηρετούνται από έναν απευθείας πίνακα που ευρετηριάζεται από το σημείο κώδικα, μια απλή αναζήτηση χωρίς αναζήτηση. Τα σημεία κώδικα στα συμπληρωματικά επίπεδα εξυπηρετούνται από έναν αραιό πίνακα ταξινομημένο κατά σημείο κώδικα και αναζητούνται με δυαδική αναζήτηση. Το αποτέλεσμα είναι ότι η GetUnicodeGlyphForCodepoint λαμβάνει ένα πλήρες Cardinal και απαντά σωστά σε ολόκληρο το εύρος, επιστρέφοντας glyph id 0, το γλυφικό .notdef, για οποιοδήποτε σημείο κώδικα δεν χαρτογραφεί η γραμματοσειρά.

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

Πού σταματούν αυτά τα ερωτήματα

Τα API απλής αντικατάστασης απαντούν σε ένα σχήμα ερώτησης, και αξίζει να είμαστε σαφείς σχετικά με το τι δεν απαντούν. Ο LookupType 1 είναι ένας από τους οκτώ τύπους αντικατάστασης. Το ερώτημα δεν χειρίζεται τον LookupType 2 multiple substitution, όπου ένα γλυφικό γίνεται πολλά, ούτε τον LookupType 4 ligature substitution, όπου πολλά γλυφικά γίνονται ένα. Δεν χειρίζεται τους contextual και chaining-contextual τύπους, LookupTypes 5 and 6, που ενεργοποιούνται μόνο όταν ένα γλυφικό εμφανίζεται σε μια συγκεκριμένη γειτονιά, ούτε τους τύπους επέκτασης (extension) και αντίστροφης αλυσίδας (reverse-chaining). Ένα διαγώνιο κλάσμα, μια σύζευξη Devanagari ή ένας αραβικός καταρράκτης αρχικού-μεσαίου-τελικού χαρακτήρα είναι πρόβλημα ακολουθίας, και μια αναζήτηση απλής αντικατάστασης ανά γλυφικό δεν μπορεί να το εκφράσει.

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

Για τις περιπτώσεις που απαιτούν εργασία σε επίπεδο ακολουθίας, η ιστορία των πολύπλοκων σεναρίων γραφής αναλύεται στο άρθρο μας για τη διαμόρφωση κειμένου πολύπλοκων σεναρίων στο Delphi. Εάν οι αντικαταστάσεις σας είναι μέρος μιας μεγαλύτερης εργασίας αναφοράς που τοποθετεί επίσης εικόνες και άλλες γραμματοσειρές στη σελίδα, ο οδηγός για την έξοδο αναφορών με γραμματοσειρές και εικόνες καλύπτει τον τρόπο με τον οποίο συνδυάζονται αυτά τα κομμάτια. Όλα αυτά εκτελούνται στην ίδια μηχανή, το HotPDF Component για Delphi και C++Builder, το οποίο φέρει τα ερωτήματα αντικατάστασης GSUB μαζί με τα API ενσωμάτωσης γραμματοσειρών, δημιουργίας υποσυνόλων και κειμένου που καλύπτονται αλλού σε αυτό το ιστολόγιο.