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

Εναλλακτικά στυλ OpenType GSUB σε καθαρό Delphi

Ένας σχεδιαστής επιλέγει μια γραμματοσειρά με μονώροφο a για επικεφαλίδες, ή ένα μηδενικό με κάθετο για πίνακες, ή ένα σετ καλλιγραφικών κεφαλαίων για εξώφυλλο. Αυτά τα γλύφα βρίσκονται ήδη στη γραμματοσειρά. Απλώς δεν είναι τα προεπιλεγμένα. Το προεπιλεγμένο a αντιστοιχίζεται από τον χαρακτήρα μέσω του πίνακα cmap σε ένα γλύφο, και η εναλλακτική λύση βρίσκεται μερικά αναγνωριστικά γλύφων μακριά, προσβάσιμη μόνο μέσω ενός κανόνα αντικατάστασης. Η παραγωγή αυτής της εναλλακτικής σε ένα PDF σημαίνει την ανάγνωση του κανόνα και την εκπομπή του γλύφου αντικατάστασης στη ροή περιεχομένου. Αυτό το άρθρο αφορά την ανάγνωση αυτών των κανόνων, του είδους της απλής αντικατάστασης, στο Object Pascal χωρίς καμία εγγενή βιβλιοθήκη διαμόρφωσης από κάτω

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

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

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

Η απλή αντικατάσταση δεν χρειάζεται μηχανή διαμόρφωσης. Χρειάζεται έναν αναλυτή για μια χούφτα μορφές υποπίνακα GSUB και μία ή δύο δυαδικές αναζητήσεις. Η συγγραφή αυτού στο Pascal διατηρεί ολόκληρη την αλυσίδα εργαλείων μέσα σε έναν μεταγλωττιστή. Το ειλικρινές όριο είναι ότι αυτή η προσέγγιση χειρίζεται αναζητήσεις αντικατάστασης γλύφων και τίποτα άλλο. Δεν είναι ανάλυση bidi, δεν είναι ινδική αναδιάταξη και δεν είναι αυτόματη διαμόρφωση βάσει περιβάλλοντος. Όπου χρειάζονται αυτά, χρειάζονται, και ένα ερώτημα απλής αντικατάστασης δεν θα τα αντικαταστήσει

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

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

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

Οι πίνακες κάλυψης αποφασίζουν ποιος συμμετέχει

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

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

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

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

Η Μορφή 2 αποθηκεύει έναν ρητό πίνακα αναγνωριστικών γλύφων αντικατάστασης. Ο δείκτης κάλυψης από τον πίνακα Coverage είναι ο δείκτης σε αυτόν τον πίνακα, επομένως ο γλύφος στον δείκτη κάλυψης 0 γίνεται η πρώτη καταχώρηση του πίνακα, ο δείκτης κάλυψης 1 η δεύτερη, και ούτω καθεξής. Η Μορφή 2 χρησιμοποιείται όταν οι εναλλακτικές δεν βρίσκονται σε ομοιόμορφη μετατόπιση, η οποία είναι η συνήθης περίπτωση για χειροποίητα σετ στυλ. Το ερώτημα είναι το ίδιο από την πλευρά του καλούντος είτε με τον έναν είτε με τον άλλον τρόπο. Πάρτε τον γλύφο εισόδου, περάστε τον από το 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;

Το συμβόλαιο που αξίζει να σημειωθεί είναι η διέλευση. Το GetSingleSubstituteGlyph επιστρέφει το αναγνωριστικό γλύφου εισόδου αμετάβλητο σε κάθε αποτυχία: χωρίς γραμματοσειρά, χωρίς πίνακα GSUB, χωρίς χαρακτηριστικό αντιστοίχισης, χωρίς κάλυψη. Αυτό σημαίνει ότι η κλήση είναι ασφαλής να γίνει χωρίς όρους. Ζητάτε την εναλλακτική, και αν δεν υπάρχει, λαμβάνετε ακριβώς αυτό που βάλατε, επομένως ο κώδικας κλήσης δεν χρειάζεται ποτέ να κάνει ειδική περίπτωση για μια γραμματοσειρά που δεν διαθέτει το χαρακτηριστικό

Τι σημαίνουν οι ετικέτες χαρακτηριστικών στυλ

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

Γύρω από αυτά υπάρχουν αρκετές ακόμη ετικέτες απλής αντικατάστασης. Το aalt είναι η πρόσβαση-σε-όλες-τις-εναλλακτικές, η ένωση κάθε εναλλακτικής που έχει ένας γλύφος, η οποία συνήθως παρουσιάζεται ως χαρακτηριστικό παλέτας γλύφων. Το titl επιλέγει κεφαλαία τίτλων κομμένα για μεγάλα μεγέθη. Τα subs και sups ανταλλάσσουν γνήσια στοιχεία δείκτη και εκθέτη αντί για μειωμένα προεπιλεγμένα. Το ordn παράγει τακτικές μορφές, τα υπερυψωμένα γράμματα στο 1st και 2nd. Το frac δημιουργεί κλάσματα, αν και τα πλήρη διαγώνια κλάσματα βασίζονται επίσης σε σύμπλεγμα (ligature) και λογική βάσει περιβάλλοντος που ξεπερνά την απλή απλή αντικατάσταση. Για τις περιπτώσεις μονού γλύφου, ο μηχανισμός είναι πανομοιότυπος με τον ss01: περάστε την ετικέτα στο ερώτημα αντικατάστασης και διαβάστε τον εναλλακτικό γλύφο

// 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 μορφή 12 και τα συμπληρωματικά επίπεδα

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

Η Μορφή 12 είναι ο υποπίνακας που καλύπτει ολόκληρο το εύρος U+0000 έως U+10FFFF. Πρόκειται για μια ταξινομημένη λίστα ομάδων, κάθε ομάδα έχει ένα σημείο κώδικα έναρξης, ένα σημείο κώδικα λήξης και ένα αναγνωριστικό γλύφου έναρξης, έτσι ώστε μια συνεχόμενη εκτέλεση σημείων κώδικα αντιστοιχίζεται σε μια συνεχόμενη εκτέλεση γλύφων. Το HotPDF επιλύει σημεία κώδικα με μια υβριδική στρατηγική που ταιριάζει με τον τρόπο με τον οποίο διαμορφώνονται τα δεδομένα. Τα σημεία κώδικα στο BMP εξυπηρετούνται από έναν άμεσο πίνακα με ευρετήριο βάσει του σημείου κώδικα, μια απλή αναζήτηση χωρίς ψάξιμο. Τα σημεία κώδικα στα συμπληρωματικά επίπεδα εξυπηρετούνται από έναν αραιό πίνακα ταξινομημένο βάσει σημείου κώδικα και αναζητούνται με μια δυαδική αναζήτηση. Το αποτέλεσμα είναι ότι το GetUnicodeGlyphForCodepoint λαμβάνει ένα πλήρες Cardinal και απαντά σωστά σε όλο το εύρος, επιστρέφοντας το αναγνωριστικό γλύφου 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, όπου ένας γλύφος γίνεται πολλοί, ούτε την αντικατάσταση συμπλέγματος LookupType 4, όπου πολλοί γλύφοι γίνονται ένας. Δεν χειρίζεται τους τύπους βάσει περιβάλλοντος και αλυσίδας βάσει περιβάλλοντος, LookupTypes 5 και 6, που ενεργοποιούνται μόνο όταν ένας γλύφος εμφανίζεται σε μια συγκεκριμένη γειτονιά, ούτε τους τύπους επέκτασης και αντίστροφης αλυσίδας. Ένα διαγώνιο κλάσμα, ένας σύνδεσμος Devanagari ή ένας αραβικός καταρράκτης αρχικός-μεσαίος-τελικός είναι ένα πρόβλημα ακολουθίας, και μια αναζήτηση απλής αντικατάστασης ανά γλύφο δεν μπορεί να το εκφράσει

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

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