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

Πρόσθετο shaping κειμένου: Uniscribe και HarfBuzz σε Delphi

Το shaping κειμένου στο στοιχείο PDFium περνά από ένα εγκαταστάσιμο object. Η ConfigureTextShaper εγκαθιστά τον shaper μέσω του οποίου δρομολογείται κάθε σημείο εισόδου shaping, αντικαθιστώντας και απελευθερώνοντας ό,τι ήταν εκεί· η ActiveTextShaper επιστρέφει τον εγκατεστημένο και δημιουργεί την προεπιλογή πλατφόρμας στην πρώτη χρήση· η ActiveTextShaperName αναφέρει ποιο backend είναι ενεργό· η ClearTextShaper απορρίπτει την εγκατάσταση και αφήνει την προεπιλογή να δημιουργηθεί ξανά. Σε Windows η προεπιλογή είναι η TPdfUniscribeTextShaper. Υπό Free Pascal υπάρχει η TPdfHarfBuzzTextShaper, που δεσμεύει το libharfbuzz κατά την εκτέλεση ώστε μια απόν βιβλιοθήκη να είναι αναφερόμενη κατάσταση και όχι αστοχία φόρτωσης

Αρχιτεκτονική πρόσθετου shaping κειμένου στο στοιχείο Delphi PDFium: ConfigureTextShaper, ActiveTextShaper και ClearTextShaper διαχειρίζονται έναν εγκατεστημένο backend, Uniscribe σε Windows και HarfBuzz δεσμευμένο κατά την εκτέλεση υπό Free Pascal
Κάθε κλήση shaping δρομολογείται μέσω του μοναδικού εγκατεστημένου object shaper, με προεπιλογή πλατφόρμας σε κάθε στόχο

Μία διασύνδεση, δύο backends που μοιράζονται τη δουλειά εντελώς διαφορετικά. Η κατανόηση εκείνης της ασυμμετρίας είναι αυτό που εμποδίζει τη φορητή διαδρομή να παράγει κείμενο που είναι shaped σωστά και τοποθετημένο λάθος

Γιατί το backend Windows είναι μία κλάση και το φορητό τρία κομμάτια;

Επειδή το Uniscribe είναι τέσσερα APIs που προσποιούνται ότι είναι ένα. Το ScriptItemize τμηματοποιεί μια συμβολοσειρά κατά script και επιλύει διδιτευθυντικά επίπεδα· το ScriptShape αντιστοιχίζει χαρακτήρες σε glyphs· το ScriptPlace υπολογίζει προόδους και αντισταθμίσεις· το ScriptLayout βάζει τις προκύπτουσες ροές σε οπτική σειρά. Ένα backend χτισμένο πάνω του δεν έχει τίποτα να προσθέσει, γι' αυτό ο shaper Windows είναι μία κλάση με μία μέθοδο

Το HarfBuzz καλύπτει τα δύο μεσαία. Διαμορφώνει και τοποθετεί μια ροή της οποίας την κατεύθυνση και το script έχει ήδη αποφασίσει ο καλών, και δεν έχει γνώμη για το πώς μια παράγραφος σχίζεται σε ροές ή σε ποια σειρά εμφανίζονται εκείνες οι ροές. Έτσι το φορητό backend προμηθεύει τα υπόλοιπα: ο διδιτευθυντικός αλγόριθμος επιλύει επίπεδα ενσωμάτωσης, οι συναρτήσεις Unicode του HarfBuzz τμηματοποιούν το κείμενο κατά script, και οι ροές διατάσσονται στην οπτική σειρά που παράγει ο κανόνας L2 του UAX #9. Το διδιτευθυντικό μισό είναι αρκετά ουσιαστικό για να είναι δική του μονάδα, που περιγράφεται στο άρθρο για τα επίπεδα ενσωμάτωσης UAX #9

Σύγκριση αγωγού shaping για κείμενο PDF: το Uniscribe προμηθεύει ScriptItemize, ScriptShape, ScriptPlace και ScriptLayout μέσα σε μία κλάση, ενώ το HarfBuzz καλύπτει μόνο shaping και τοποθέτηση γύρω από τα δικά του στάδια UAX #9 του στοιχείου
Το Uniscribe καλύπτει και τα τέσσερα στάδια· η φορητή διαδρομή πρέπει να προμηθεύει το ίδιο τμηματοποίηση και οπτική σειρά

Ο shaper δεν επιλύει γραμματοσειρές, και είναι σκόπιμο

Το Uniscribe διαβάζει το δυαδικό αρχείο της γραμματοσειράς από ένα device context GDI. Δεν υπάρχει φορητό αντίστοιχο του ίδιου, και η εφεύρεση ενός μέσα σε μονάδα shaping θα σήμαινε απόφαση εκ μέρους κάθε εφαρμογής αν οι γραμματοσειρές προέρχονται από fontconfig, από CoreText, από φάκελο γραμματοσειρών εφαρμογής, ή από βάση δεδομένων. Έτσι το backend HarfBuzz παίρνει έναν resolver: μια callback που αντιστοιχίζει όνομα γραμματοσειράς στα bytes TrueType ή OpenType. Η επιστροφή False αποτυγχάνει το αίτημα shaping με τον ίδιο τρόπο που αποτυγχάνει μια μη αναγνώσιμη γραμματοσειρά GDI σε Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Η πολιτική σας: fontconfig, CoreText, φάκελος γραμματοσειρών
  // εφαρμογής, βάση δεδομένων
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Η ιδιοκτησία περνά στη μονάδα· καλέστε μία φορά κατά την
  // εκκίνηση, πριν οτιδήποτε shape κείμενο
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // Σε Delphi η προεπιλογή πλατφόρμας (Uniscribe) δημιουργείται
  // κατ' απαίτηση, οπότε δεν χρειάζεται καθόλου εγκατάσταση
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Το να κρατείται η ανακάλυψη γραμματοσειρών έξω από τον shaper έχει δεύτερο όφελος που εμφανίζεται σε διακομιστές: η ίδια διεργασία μπορεί να διαμορφώνει κείμενο με σύνολο ενσωματωμένων γραμματοσειρών που δεν έχει καμία σχέση με ό,τι είναι εγκατεστημένο στο μηχάνημα, που είναι αυτό που θέλετε όταν η έξοδος πρέπει να είναι byte-αναπαραγώγιμη μεταξύ hosts. Το στοιχείο εκθέτει επίσης πάροχο γραμματοσειρών συστήματος host για τις περιπτώσεις που θέλετε πράγματι εγκατεστημένες γραμματοσειρές, που καλύπτεται στο άρθρο για τον πάροχο γραμματοσειρών συστήματος

Το record αποτελέσματος είναι ουδέτερο ως προς backend, και τα clusters είναι ο λόγος

Και τα δύο backends γεμίζουν το ίδιο TPdfShapedText: το κείμενο πηγής, όνομα γραμματοσειράς, μέγεθος, bytes γραμματοσειράς, έναν πίνακα ροών, το συνολικό πλάτος, τον αριθμό glyphs και τον αριθμό λογικών χαρακτήρων. Κάθε TPdfShapedRun κουβαλά το τμήμα του στο κείμενο πηγής, την οπτική θέση X του, το πλάτος του, το διδιτευθυντικό επίπεδό του και σημαία από δεξιά προς αριστερά, συν τα glyphs του. Κάθε TPdfShapedGlyph κουβαλά αναγνωριστικό glyph, μια πρόοδο, αντισταθμίσεις X και Y, και το cluster στο οποίο ανήκει ως αρχή και μήκος στο κείμενο πηγής

Εκείνα τα πεδία cluster είναι αυτό που κάνει το record χρησιμοποιήσιμο και όχι απλώς πληροφοριακό. Το shaping δεν είναι αντιστοίχιση ένα προς ένα: μια συλλαβή Devanagari γίνεται ένα glyph από τέσσερις χαρακτήρες, ένα Arabic ligature συγχωνεύει δύο, και ένας μοναδικός χαρακτήρας μπορεί να παράγει αρκετά σημεία. Χωρίς τμήματα cluster δεν μπορείτε να τοποθετήσετε caret, να κάνετε hit-test σε κλικ, ή να επισημάνετε επιλογή, επειδή δεν μπορείτε να πείτε σε ποιους χαρακτήρες ανήκει ένα glyph. Μαζί τους, η αριθμητική είναι τοπική και ο ίδιος κώδικας δουλεύει και για τα δύο backends

Τμήματα cluster glyphs στο TPdfShapedText: ένα glyph συλλαβής Devanagari από τέσσερις χαρακτήρες, ένα Arabic ligature από δύο, και βάση συν σημείο από έναν χαρακτήρα, το καθένα αντιστοιχισμένο πίσω μέσω ClusterStart και ClusterLength
Τα τμήματα cluster αντιστοιχίζουν κάθε glyph πίσω στους χαρακτήρες πηγής του ώστε carets, hit tests και επιλογές να δουλεύουν
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // Οι ροές φτάνουν ήδη σε οπτική σειρά με VisualX συμπληρωμένο
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

Τα budgets ανήκουν στο record επιλογών

Το TPdfTextShapingOptions κουβαλά μια κατεύθυνση συν τρία όρια: μέγιστοι χαρακτήρες, μέγιστα glyphs και μέγιστες ροές, με κλάση-συνάρτηση Default που γεμίζει λογικές τιμές. Τα όρια δεν είναι παράνοια για κακοδιατυπωμένη είσοδο· είναι αριθμητική. Το shaping διαστέλλει: μια γραμματοσειρά με επιθετική contextual αντικατάσταση μπορεί να εκπέμψει περισσότερα glyphs από χαρακτήρες εισόδου, και μια παράγραφος που εναλλάσσει scripts κάθε λίγους χαρακτήρες παράγει μια ροή ανά εναλλαγή. Ένα έγγραφο συναρμολογημένο για να μεγιστοποιήσει και τα δύο μετατρέπει μια μέτρια συμβολοσειρά σε μεγάλη δέσμευση, και μια υπηρεσία που διαμορφώνει κείμενο από μη αξιόπιστα PDF χρειάζεται όριο που διάλεξε εκείνη και όχι όριο που επιβάλλει το μηχάνημα

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

Δέσμευση κατά την εκτέλεση, όχι εξάρτηση build

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

Ο αντίστοιχος κανόνας για καλούντες είναι να ελέγχουν. Η ActiveTextShaper επιστρέφει nil όταν η πλατφόρμα δεν έχει προεπιλογή και καμία δεν ρυθμίστηκε, και το σημείο εισόδου shaping αναφέρει αυτό ως μη διαθέσιμο shaper και όχι ως αστοχία shaping. Εκείνα είναι διαφορετικά προβλήματα και αξίζουν διαφορετικά μηνύματα: το ένα είναι κενό ανάπτυξης, το άλλο είναι πρόβλημα γραμματοσειράς ή κειμένου

Εγκαταστήστε μία φορά, πριν οτιδήποτε shape

Η εγκατάσταση αντικαθιστά και απελευθερώνει τον προηγούμενο shaper, οπότε η επανειλημμένη κλήση είναι ασφαλής αλλά άσκοπη, και η κλήση ενώ άλλο νήμα διαμορφώνει κείμενο δεν είναι καθόλου ασφαλής. Κάντε το κατά την εκκίνηση. Αν χρειάζεστε πτώση πίσω στην προεπιλογή πλατφόρμας αργότερα, περάστε nil, που είναι επίσης ο τρόπος να αναιρέσετε ένα test double στο τέλος ενός test

Μόλις εγκατασταθεί backend, μέτρηση και αναδίπλωση συμπεριφέρονται το ίδιο και στις δύο πλατφόρμες, αφού καταναλώνουν τις μετρικές ροής και glyph αντί να καλούν την πλατφόρμα ευθεία· το μοντέλο αναδίπλωσης περιγράφεται στο άρθρο για τη μέτρηση κειμένου και την αναδίπλωση λέξεων. Οι υποστηριζόμενες πλατφόρμες και toolchains για το στοιχείο καταγράφονται στη σελίδα προϊόντος PDFium Delphi component