Το Uniscribe κάνει περισσότερη δουλειά απ' όση συνειδητοποιούν οι περισσότεροι καλούντες. Το ScriptItemize εκτελεί διδιτευθυντική ανάλυση και τμηματοποίηση script σε ένα πέρασμα, και το ScriptLayout παράγει την οπτική σειρά των προκυπτουσών ροών. Το HarfBuzz, ο φορητός αντικαταστάτης στον οποίο καταφεύγει κόσμος, δεν κάνει ούτε το ένα: διαμορφώνει μία ροή της οποίας η κατεύθυνση και το script έχουν ήδη αποφασιστεί από κάποιον άλλον. Έτσι το δύσκολο μέρος του να πάει μια αγωγή κειμένου PDF των Windows σε Linux ή macOS δεν είναι η δέσμευση μιας μηχανής shaping. Είναι η προμήθεια του διδιτευθυντικού αλγορίθμου που το Uniscribe παρείχε ήσυχα, και στο στοιχείο PDFium αυτό είναι ο σκοπός του FPdfBidi
Η μονάδα υλοποιεί το UAX #9 ευθεία: τους κανόνες P2 και P3 για κατεύθυνση παραγράφου, X1 έως X10 για ρητές ενσωματώσεις και isolates, W1 έως W7 για αδυνείς τύπους, N0 έως N2 για ουδέτερα και αγκύλες, I1 και I2 για σιωπηλά επίπεδα, και L1 και L2 για την τελική αναδιοργάνωση. Δύο συναρτήσεις το κουβαλάν: η PdfResolveBidiLevels επιστρέφει ένα επίπεδο ενσωμάτωσης ανά μονάδα κώδικα UTF-16, και η PdfBidiVisualOrder μετατρέπει εκείνα τα επίπεδα στη μετάθεση που τοποθετεί μονάδες κώδικα από αριστερά προς τα δεξιά
Τι σας δίνει ο αλγόριθμος, και τι όχι
Σας δίνει αριθμούς. Τα άρτια επίπεδα είναι από αριστερά προς τα δεξιά, τα περιττά από δεξιά προς αριστερά, και το επίπεδο κάθε χαρακτήρα κωδικοποιεί τη φωλιά των διδιτευθυντικών ροών μέσα στις οποίες κάθεται ο χαρακτήρας. Από εκείνους τους αριθμούς το L2 εξάγει μια μετάθεση. Αυτό που ο αλγόριθμος δεν κάνει σκόπιμα είναι να αποφασίσει ποια γραμματοσειρά να χρησιμοποιήσει, να σχηματίσει ligatures, ή να αναδιοργανώσει glyphs μέσα σε cluster· εκείνα είναι ζητήματα shaping και ανήκουν στο στάδιο μετά από αυτό
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// Το pbdAuto εφαρμόζει P2-P3: ο πρώτος ισχυρός χαρακτήρας αποφασίζει
if PdfResolveBidiLevels(Text, pbdAuto, Levels, ParagraphLevel) then
begin
Order := PdfBidiVisualOrder(Text, Levels);
SetLength(Visual, Length(Order));
for I := 0 to High(Order) do
Visual[I + 1] := Text[Order[I] + 1];
// Το Visual πλέον διαβάζεται από αριστερά προς τα δεξιά· το Levels[]
// λέει ακόμη ποιες ροές είναι RTL ώστε ένας shaper να πάρει σωστές
// κατευθύνσεις
end;
end;
Ο πίνακας κλάσεων χαρακτήρων παράγεται, δεν γράφεται
Κάθε code point έχει ιδιότητα Bidi_Class, και ο αλγόριθμος τη συμβουλεύεται διαρκώς, οπότε ο πίνακας είναι το θεμέλιο πάνω στο οποίο στέκεται όλα τα άλλα. Παράγεται από το Unicode Character Database και όχι συντηρείται με το χέρι: το πέμπτο πεδίο του UnicodeData.txt δίνει τις ανατεθειμένες κλάσεις, και οι δηλώσεις @missing στο DerivedBidiClass.txt δίνουν τις προεπιλογές για code points που η βάση δεν αναθέτει, που είναι ο τρόπος που τα μη κατανεμημένα blocks προεπιλέγονται σωστά σε R, AL, ET ή BN αντί για L
Το κόλπο συμπίεσης είναι να εκπέμπονται μόνο τα εύρη των οποίων η κλάση δεν είναι L. Οτιδήποτε πέφτει έξω από κάθε εύρος είναι L, που είναι και η προεπιλογή Unicode και η κλάση της συντριπτικής πλειονότητας των code points. Αυτό παίρνει έναν πίνακα που διαφορετικά θα έτρεχε σε χιλιάδες εγγραφές κάτω σε 745 εύρη και περίπου 6,7 KB. Η λειτουργική συνέπεια αξίζει να δηλωθεί: όταν μεταβείτε σε νέα έκδοση Unicode, επαναλάβετε τον generator. Η επεξεργασία του αρχείου include με το χέρι θα δούλευε, και θα αποκλίνει επίσης σιωπηλά από τη βάση στην επόμενη αναβάθμιση
Το L2 πρέπει να αναδιοργανώνει code points, όχι μονάδες κώδικα UTF-16
Αυτό είναι το λάθος που παράγει πραγματικά κατεστραμμένη έξοδο, και η πρώτη υλοποίηση το έκανε. Το L2 λέει να αντιστραφούν συνεχόμενες ροές σε κάθε επίπεδο από το υψηλότερο κάτω έως το χαμηλότερο περιττό επίπεδο. Γραμμένο πάνω σε συμβολοσειρά UTF-16, «αντιστροφή μιας ροής» σημαίνει φυσικά την αντιστροφή των μονάδων κώδικα μέσα της. Για χαρακτήρες στο Basic Multilingual Plane είναι εντάξει. Για έναν χαρακτήρα RTL σε astral plane, όπως εκείνους στα blocks Κυπριακά ή Αρχαία Νοτιοαραβικά κοντά στο U+10800, δεν είναι: ο χαρακτήρας είναι ζεύγος surrogates, η αντιστροφή της ροής βάζει το low surrogate πριν από το high, και η συμβολοσειρά περιέχει πλέον δύο αζευγμένα surrogates αντί για έναν χαρακτήρα. Τίποτα κατάντι δεν μπορεί να το ανακτήσει
Η διόρθωση είναι να γίνεται το L2 σε μονάδες code points. Η υλοποίηση συγχωνεύει μονάδες κώδικα σε μονάδες code points, εκτελεί τις αντιστροφές πάνω σε εκείνες τις μονάδες, και αναπτύσσει το αποτέλεσμα πίσω σε δείκτες μονάδων κώδικα στο τέλος. Γι' αυτό η PdfBidiVisualOrder παίρνει το κείμενο και όχι μόνο τον πίνακα επιπέδων: δεν μπορεί να πει από τα επίπεδα μόνα πού βρίσκονται τα όρια surrogates. Η ίδια πειθαρχία ζευγών surrogates διατρέχει γενικά τα API κειμένου, όπως περιγράφεται στο άρθρο για emoji, CJK και ζεύγη surrogates
Η κατάβαση μέσω επιπέδων πρέπει να περιλαμβάνει επίπεδα που δεν εμφανίζονται
Το δεύτερο λάθος είναι πιο λεπτό και δεν παράγει κατάρρευση, απλώς κείμενο που δεν αναδιοργανώθηκε. Το L2 λέει να ξεκινήσει από το υψηλότερο παρόν επίπεδο και να κατέβει έως το χαμηλότερο περιττό επίπεδο. Μια φυσική βελτιστοποίηση είναι να συλλεχθεί το σύνολο των επιπέδων που εμφανίζονται πράγματι και να επαναληφθεί πάνω σε εκείνο το σύνολο. Είναι λάθος
Σκεφτείτε μια γραμμή λατινικού κειμένου μέσα σε μια ενσωμάτωση από δεξιά προς αριστερά. Το επίπεδο παραγράφου είναι 0, η ενσωμάτωση σπρώχνει τους λατινικούς χαρακτήρες στο επίπεδο 2, και κανένας χαρακτήρας δεν κάθεται στο επίπεδο 1. Η επανάληψη πάνω σε εμφανιζόμενα επίπεδα βρίσκει μόνο 0 και 2, και δεν υπάρχει καθόλου περιττό επίπεδο, οπότε ο βρόχος δεν εκτελεί καμία αντιστροφή. Εκείνη η απάντηση είναι σωστή, αλλά για λόγο που η βελτιστοποίηση δεν γνωρίζει: μια αντιστροφή στο επίπεδο 2 ακολουθούμενη από αντιστροφή στο επίπεδο 1 θα ακύρωνε ακριβώς, οπότε η εκτέλεση ούτε του ενός ούτε του άλλου είναι το σωστό αποτέλεσμα. Αλλάξτε την είσοδο ελαφρώς, ώστε να υπάρχουν χαρακτήρες επιπέδου 1 και επιπέδου 3 αλλά όχι επιπέδου 2, και ο βρόχος βάσει συνόλου παραλείπει την αντιστροφή επιπέδου 2 που ο αλγόριθμος απαιτεί
// Σωστό: περπατήστε κάθε επίπεδο από το μέγιστο κάτω έως το
// χαμηλότερο περιττό επίπεδο, συμπεριλαμβανομένων επιπέδων που
// κανένας χαρακτήρας δεν έχει πράγματι
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op όταν καμία ροή δεν πληροί όρους
Dec(Level);
end;
Γραμμένος ως απλός φθίνων βρόχος η συμπεριφορά προκύπτει δωρεάν, και οι επαναλήψεις no-op δεν κοστίζουν μετρήσιμα. Αυτή είναι περίπτωση όπου η προφανής βελτιστοποίηση δεν είναι ελαφρώς λάθος, είναι λάθος με τρόπο εξαρτώμενο από είσοδο που ένα μικρό σώμα test δεν θα αποκάλυπτε ποτέ
Αγκύλες: BD16 με πρακτικό πίνακα
Ο κανόνας N0 και ο αλγόριθμος ζευγών αγκυλών BD16 υπάρχουν ώστε μια παρένθεση σε κείμενο μικτής κατεύθυνσης να επιλύεται στην κατεύθυνση αυτού που περικλείει και όχι σε ό,τι τυχαίνει να είναι γειτονικό. Αυτό χρειάζεται πίνακα ζευγών αγκυλών. Η υλοποίηση κουβαλά τα ζεύγη σε γενική χρήση και όχι το πλήρες περιεχόμενο του αρχείου αγκυλών Unicode: ASCII, CJK, fullwidth, μαθηματικές και διακοσμητικές αγκύλες
Μια μη καταγεγραμμένη αγκύλη δεν είναι σφάλμα. Επιλύεται ως συνηθισμένο ουδέτερο μέσω N1 και N2, που είναι ακριβώς η συμπεριφορά που είχε κάθε υλοποίηση πριν το Unicode 6.3 εισήγαγε το N0. Έτσι το όριο είναι «λιγότερο εκλεπτυγμένο για σπάνιες αγκύλες», όχι «λάθος». Μια λεπτομέρεια χρειάζεται ρητό χειρισμό: η κανονική ισοδυναμία ανάμεσα στις γωνιακές αγκύλες στα U+2329 και U+232A και εκείνες στα U+3008 και U+3009 πρέπει να διπλώνεται κατά την αντιστοίχιση ζευγών, αλλιώς μια ανοιγόμενη αγκύλη γραμμένη με τον έναν τρόπο δεν θα ταίριαζε με κλεινόμενη γραμμένη με τον άλλον
Πώς τεστάρετε τριάντα αλληλεπιδρώντες κανόνες
Όχι με μεγάλο σώμα δεδομένων, τουλάχιστον όχι πρώτα. Η παραγωγική προσέγγιση ήταν δεκάξι χειροκίνητα επαληθευμένες περιπτώσεις, η καθεμία διαλεγμένη να ασκεί συγκεκριμένο κανόνα και η καθεμία ελεγμένη απέναντι στα επίπεδα που λέει το UAX #9 ότι πρέπει να παράγει: ανίχνευση κατεύθυνσης παραγράφου υπό P2 και P3, οι κανόνες αδυνών τύπων W2, W3 και W7, οι κανόνες σιωπηλών επιπέδων I1 και I2, ρητή ενσωμάτωση μέσω X2 και X7, isolates μέσω X5a και X6a, η επαναφορά L1 των τελικών κενών και διαχωριστικών, μια περίπτωση αγκύλης N0, και μία περίπτωση με astral χαρακτήρα για να κλειδώσει ο χειρισμός surrogates
Δεκαέξι περιπτώσεις με γνωστά-σωστά αναμενόμενα επίπεδα πιάνουν περισσότερα από χίλια εξακόσια cases με πειστικά φαινόμενης εμφάνισης έξοδο, επειδή η λειτουργία αστοχίας μιας διδιτευθυντικής υλοποίησης είναι κείμενο που διαβάζεται σχεδόν σωστά. Μόλις εκείνα περάσουν, ένα σώμα δεδομένων είναι χρήσιμο για εύρεση κενών πίνακα και προβλημάτων απόδοσης, που είναι διαφορετικές κατηγορίες ελαττωμάτων
Μέσα στο στοιχείο PDFium τα επίπεδα τροφοδοτούν δύο καταναλωτές. Στην πλευρά γραφής λένε στο backend shaping την κατεύθυνση κάθε ροής, που είναι η είσοδος που απαιτεί το HarfBuzz. Στην πλευρά ανάγνωσης ενημερώνουν τη γεωμετρία επιλογής και τη σειρά ανάγνωσης, αφού ένα κλικ σε RTL κείμενο πρέπει να αντιστοιχιστεί σε λογική θέση και όχι σε οπτική· εκείνη η αντιστοίχιση καλύπτεται στο άρθρο για την επιλογή οπτικής γραμμής και το μοντέλο σειράς ανάγνωσης στα δομημένα blocks κειμένου και τη σειρά ανάγνωσης. Οι λεπτομέρειες υποστήριξης πλατφορμών του στοιχείου βρίσκονται στη σελίδα προϊόντος PDFium Delphi component