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

Διαμόρφωση Αραβικού και RTL Κειμένου σε PDF με Delphi και HotPDF

Περάστε την αραβική φράση يوضح ملف PDF στο TextOut και ανοίξτε το αποτέλεσμα. Τα γράμματα τρέχουν προς τη λάθος κατεύθυνση, και το καθένα κάθεται στην απομονωμένη του μορφή με ένα ορατό κενό πριν από το επόμενο, σαν κάποιος να πληκτρολόγησε Αγγλικά ανάποδα και να πάτησε το διάστημα ανάμεσα σε κάθε χαρακτήρα. Δεν ενεργοποιήθηκε καμία εξαίρεση. Δεν εκτυπώθηκε καμία προειδοποίηση. Η έξοδος είναι απλά λάθος, και είναι λάθος επειδή ποτέ δεν πραγματοποιήθηκαν δύο ξεχωριστοί μετασχηματισμοί από τους οποίους εξαρτώνται τα Αραβικά. Το να γνωρίζετε ποιοι είναι αυτοί οι δύο μετασχηματισμοί, και ποια κλήση τους εκτελεί, είναι η ουσία της εξόδου PDF πολύπλοκων γραφών (complex-script)

Το HotPDF είναι ένα εγγενές VCL PDF στοιχείο (component) για Delphi και C++Builder, και κάνει τη δεξιά-προς-αριστερά εργασία για εσάς μέσω μιας ξεχωριστής κλήσης. Σταματά επίσης σε μερικά συγκεκριμένα σημεία τα οποία θέλετε να γνωρίζετε πριν δεσμευτείτε σε μια τοπική ρύθμιση (locale), οπότε αυτό το άρθρο χαρτογραφεί τις έννοιες και τα πραγματικά όρια· η πρακτική ρύθμιση για την ίδια την κλήση βρίσκεται στο άρθρο αναφοράς του RtLTextOut

Γιατί μια σωστή συμβολοσειρά εξακολουθεί να εκτυπώνεται λάθος

Το Unicode διατηρεί το κείμενο σε λογική σειρά, τη σειρά με την οποία το πληκτρολογείτε και το διαβάζετε φωναχτά. Ένας renderer πρέπει να τοποθετήσει τα γλυφικά (glyphs) σε οπτική σειρά. Για γραφές από αριστερά προς τα δεξιά αυτές οι σειρές συμπίπτουν και κανείς δεν το σκέφτεται. Για τα Αραβικά και τα Εβραϊκά δεν συμβαίνει αυτό, και όταν μια γραμμή αναμειγνύει κατευθύνσεις, ας πούμε μια αραβική πρόταση που φέρει το λατινικό διακριτικό "PDF" ή μια τιμή γραμμένη με ψηφία, ο Αλγόριθμος Αμφίδρομης Γραφής Unicode (UAX #9) αποφασίζει ακριβώς πώς τα θραύσματα από αριστερά προς τα δεξιά φωλιάζουν μέσα στη γραμμή από δεξιά προς τα αριστερά. Αυτός είναι ο πρώτος μετασχηματισμός, η αναδιάταξη (reordering), και η παράλειψή του είναι αυτή που αναποδογυρίζει τη γραμμή

Ο δεύτερος είναι η διαμόρφωση βάσει πλαισίου (contextual shaping). Ένα αραβικό γράμμα σχεδιάζεται διαφορετικά ανάλογα με το πού βρίσκεται μέσα σε μια λέξη: αρχικό, μεσαίο, τελικό ή απομονωμένο. Το codepoint παραμένει το ίδιο παντού· μόνο το γλυφικό αλλάζει. Μια ροή επεξεργασίας που παραδίδει κάθε codepoint κατευθείαν στο προεπιλεγμένο γλυφικό του παράγει ακριβώς την αποσυνδεδεμένη έξοδο, σε απομονωμένη μορφή, από την εναρκτήρια παράγραφο. Τα Εβραϊκά παραλείπουν αυτό το βήμα, αφού τα γράμματά τους δεν ενώνονται, αλλά εξακολουθούν να χρειάζονται την αναδιάταξη. Τα Αραβικά χρειάζονται και τα δύο, και γι' αυτό τα Αραβικά, όχι τα Εβραϊκά, είναι η συμβολοσειρά (string) με την οποία δοκιμάζετε

Στην επιφάνεια εργασίας τίποτα από αυτά δεν είναι δικό σας πρόβλημα. Όταν μια φόρμα VCL σχεδιάζει Αραβικά σε ένα TEdit, η στοίβα κειμένου (text stack) του λειτουργικού συστήματος την αναδιατάσσει και τη διαμορφώνει αθόρυβα, πράγμα που είναι ακριβώς ο λόγος για τον οποίο η συμβολοσειρά που φαίνεται τέλεια στην οθόνη βγαίνει σπασμένη σε ένα απλό PDF. Μια ροή περιεχομένου (content stream) δεν αποθηκεύει επεξεργάσιμο κείμενο. Αποθηκεύει τοποθετημένα γλυφικά, οπότε όποιος εκπέμπει τη ροή κληρονομεί τη δουλειά της διαμόρφωσης που παλιά χειριζόταν το OS. Το RtLTextOut είναι η κλήση που παίρνει πίσω αυτή τη δουλειά

Τι διαμορφώνει το RtLTextOut για εσάς

Το HotPDF διατηρεί τη λατινική διαδρομή και τη διαδρομή πολύπλοκης γραφής ως δύο διαφορετικές μεθόδους. Το TextOut εκτυπώνει αυτό που του δίνετε με τη σειρά που του το δίνετε. Το RtLTextOut εκτελεί πρώτα και τους δύο μετασχηματισμούς — αμφίδρομη αναδιάταξη σε όλη τη γραμμή, ανάλυση πλαισίου για τις ενωμένες γραφές — και μετά εκτυπώνει. Το ποιας γραφής οι κανόνες εφαρμόζονται ταξιδεύει μέσα από το charset της γραμματοσειράς και όχι μέσω της ίδιας της κλήσης, έτσι η κατεύθυνση είναι μια ρητή επιλογή σε κάθε σημείο κλήσης αντί για μια μαντεψιά που γίνεται από τους χαρακτήρες. Η ρύθμιση παράμετρο προς παράμετρο, οι τιμές charset, τα βήματα καταχώρισης γραμματοσειράς και ένα πλήρες μεταγλωττίσιμο (compilable) παράδειγμα βρίσκονται όλα στο άρθρο αναφοράς του RtLTextOut· αυτό το κομμάτι παραμένει στο τι σημαίνουν οι μετασχηματισμοί, πού σταματούν και πώς να αποδείξετε ότι λειτούργησαν

Ένας κανόνας χρήσης έχει σημασία ακόμη και σε αυτό το επίπεδο: η είσοδος πρέπει να είναι σε λογική σειρά, επειδή το RtLTextOut εκτελεί την αναστροφή το ίδιο, και μια συμβολοσειρά που έχετε ήδη αναστρέψει με το χέρι βγαίνει διπλά ανεστραμμένη — το άρθρο αναφοράς σας καθοδηγεί μέσα από αυτήν την παγίδα και τον καθαρισμό της. Αυτό που κερδίζει στην παγίδα μια αναφορά εδώ είναι γιατί επιβιώνει από τις δοκιμές. Μια διπλά ανεστραμμένη συμβολοσειρά καθαρών Αραβικών μπορεί να φαίνεται απόλυτα σωστή, και διαλύεται μόνο όταν μια γραμμή φέρει μια λατινική λέξη ή έναν αριθμό, επειδή αυτά τα ενσωματωμένα τμήματα (runs) δεν φωλιάζουν πλέον με τον τρόπο που υπαγορεύει ο UAX #9. Το σφάλμα δεν βρίσκεται στην απόδοση (rendering)· βρίσκεται στην τροφοδότηση του αλγορίθμου με κείμενο που είχε ήδη υποστεί μισή επεξεργασία

Η ίδια αυτή συμπεριφορά μικτής κατεύθυνσης μπερδεύει τους κριτές περισσότερο από ό,τι μπερδεύει τον κώδικα. Μέσα σε μια γραμμή από δεξιά προς τα αριστερά, τα ψηφία και οι ενσωματωμένες λατινικές λέξεις εξακολουθούν να διαβάζονται από αριστερά προς τα δεξιά. Κάποιος που δεν έχει δουλέψει με αμφίδρομη διάταξη (bidirectional layout) θα κοιτάξει ένα αποδοθέν τιμολόγιο, θα δει τον αριθμό λογαριασμού να διαβάζεται με το «λάθος» τρόπο σε σχέση με τα Αραβικά γύρω του, και θα το καταγράψει ως σφάλμα. Είναι το σωστό αποτέλεσμα βάσει των προδιαγραφών. Μια σύντομη σημείωση στα κριτήρια αποδοχής (acceptance criteria) σας, γραμμένη πριν από την πρώτη διέλευση από φυσικό ομιλητή, σας γλιτώνει από αυτήν την περιττή διαδικασία

Πότε η αναδιάταξη και η ένωση (joining) είναι αρκετές, και πότε δεν είναι

Για συνεχές Αραβικό και Εβραϊκό κείμενο — αναφορές, τιμολόγια, συμβόλαια, επιστολές — η αναδιάταξη συν την ένωση βάσει πλαισίου είναι όλη η δουλειά, και το RtLTextOut την φέρει μόνο του. Το όριο εμφανίζεται όταν η τυπογραφία ζητά περισσότερα από την ένωση. Η απάντηση του HotPDF στην αραβική πλευρά είναι ένας προαιρετικός (opt-in) διαμορφωτής από την πλευρά του παραγωγού: ορίστε AutoShapeArabic := True και το component ξαναγράφει τη λογικής σειράς ροή (run) σε Unicode Presentation Forms πριν από την αμφίδρομη διέλευση (bidirectional pass), έτσι οι μορφές ένωσης υπολογίζονται έναντι των λογικών γειτόνων και οι πτυχώσεις συμπλεγμάτων (ligatures) ενσωματώνονται (baked) στα codepoints που φέρει στην πραγματικότητα το PDF, αντί να αφεθούν σε έναν viewer για να τα επιλύσει. Ο διακόπτης είναι από προεπιλογή στο off και η έξοδος είναι byte-stable (σταθερή σε επίπεδο byte) όταν παραμένει στο off, οπότε η ενεργοποίησή του είναι μια σκόπιμη απόφαση ανά ροή επεξεργασίας εγγράφου, όχι μια καθολική αναβάθμιση. Το ίδιο προαιρετικό μοντέλο επεκτείνεται και στις άλλες ενωμένες γραφές από δεξιά προς τα αριστερά που διαμορφώνει το HotPDF: Τα Συριακά, N'Ko, Adlam, και Hanifi Rohingya έχουν το καθένα τη δική του σημαία αυτόματης διαμόρφωσης που αντικατοπτρίζει την Αραβική

Οι προαιρετικές δυνατότητες OpenType είναι πάλι ένας διαφορετικός μηχανισμός. Τα διακριτικά συμπλέγματα (discretionary ligatures) και παρόμοιες δυνατότητες μονής αντικατάστασης (single-substitution) περνούν μέσα από το GetSingleSubstituteGlyph(GID, 'liga'), το οποίο επιλύει μία αντικατάσταση τη φορά — ID γλυφικού εισόδου πρώτα, ετικέτα χαρακτηριστικού δεύτερη — και επιστρέφει το γλυφικό εισόδου αμετάβλητο όταν το χαρακτηριστικό δεν ισχύει. Αυτό είναι αρκετό για να οδηγήσει μια γνωστή, πεπερασμένη λίστα συμπλεγμάτων που συντηρείτε μόνοι σας. Δεν είναι μια πλήρης μηχανή GSUB (Γλυφική Αντικατάσταση), και η διαφορά είναι ακριβώς εκεί όπου πάνε στραβά τα φιλόδοξα σχέδια για τοπικές ρυθμίσεις (locales): μια ροή διαμόρφωσης που χειρίζεται άψογα τα Αραβικά έχει επιδείξει αναδιάταξη και ένωση, τίποτα περισσότερο

Κάλυψη μεταξύ γραφών

Τα Αραβικά ασκούν και τους δύο μετασχηματισμούς, γι' αυτό και είναι η συμβολοσειρά για δοκιμή, και γι' αυτό μια δοκιμή στα Αραβικά είναι η ισχυρότερη μεμονωμένη απόδειξη ότι η ροή λειτουργεί. Τα Εβραϊκά χρειάζονται την αναδιάταξη αλλά όχι την ένωση, αφού τα γράμματά τους στέκονται μόνα τους· αν τα Εβραϊκά αποδίδονται σωστά αλλά τα Αραβικά βγαίνουν αποσυνδεδεμένα, το αμφίδρομο μισό είναι εντάξει και το μισό του πλαισίου δεν έτρεξε ποτέ. Τα Περσικά και τα Ούρντου χρησιμοποιούν την αραβική γραφή και κληρονομούν τη συμπεριφορά της, αν και η προτίμηση των Ούρντου για το στυλ Nastaliq είναι μια απόφαση γραμματοσειράς με συνέπειες στην αναγνωσιμότητα που πρέπει να κρίνει ένας φυσικός ομιλητής

Τα Ταϊλανδικά βρίσκονται εντελώς στην άλλη πλευρά της γραμμής. Τρέχουν από αριστερά προς τα δεξιά, οπότε δεν χρειάζονται αμφίδρομη εργασία, και τα γράμματά τους δεν ενώνονται, οπότε δεν χρειάζονται ανάλυση πλαισίου· οι Ταϊλανδικές συμβολοσειρές περνούν μέσα από την κανονική διαδρομή TextOut όπως τα Λατινικά. Αυτό που έχουν τα Ταϊλανδικά είναι στοιβαγμένα σημάδια (stacked marks) — φωνήεντα και τονικά σημάδια πάνω και κάτω από το βασικό σύμφωνο — και το αν αυτά κάθονται σωστά εξαρτάται από τη γραμματοσειρά που δημιουργεί τα σημάδια συνδυασμού της (combining marks) για να στοιβάζονται χωρίς τη βοήθεια της μηχανής διαμόρφωσης (shaping-engine). Οι περισσότερες εξειδικευμένες γραμματοσειρές της Ταϊλανδικής το κάνουν. Δοκιμάστε με την ακριβή γραμματοσειρά που θα ενσωματώσετε, όχι με μια παρόμοια

Τα Ντεβαναγκάρι και η υπόλοιπη οικογένεια των Ινδικών (Indic) αποτελούν το πραγματικό σκληρό όριο. Τα σημάδια φωνηέντων τους αναδιατάσσονται γύρω από συμπλέγματα συμφώνων και τα συμπλέγματά (conjuncts) τους σχηματίζονται μέσα από αλυσίδες εξαρτώμενων από το πλαίσιο αντικαταστάσεων, που είναι περιοχή πλήρους GSUB, πέρα από την αναδιάταξη και την ένωση. Αν μια ινδική τοπική ρύθμιση είναι στον χάρτη πορείας, εκτελέστε ένα πραγματικό πιλοτικό σε γνήσιες συμβολοσειρές πελατών πριν το υποσχεθείτε — το ότι δουλεύουν τα Αραβικά δεν αποτελεί απόδειξη ότι θα δουλέψουν και τα Ντεβαναγκάρι. Οι συμβολοσειρές CJK, τα Βιετναμέζικα με τα στοιβαγμένα διακριτικά (diacritics) τους, και το μικτό Ευρωπαϊκό κείμενο όλα παίρνουν την κανονική διαδρομή χωρίς αμφίδρομη ανάλυση, και αξίζει τον κόπο να διατηρείτε τις δύο διαδρομές φυσικά ξεχωριστές στον κώδικα της αναφοράς, μια ρουτίνα για ροές RTL (RTL runs) και μία για όλα τα υπόλοιπα, ώστε η λογική της τοπικής ρύθμισης να είναι ορατή στο σημείο κλήσης αντί να κρύβεται πίσω από μια σημαία που κάποιος θα ξεχάσει να ορίσει

Η κάλυψη γλυφικών αποφασίζεται πριν καν τρέξει η διαμόρφωση

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

// After RegisterUnicodeTTF, audit coverage for the
// codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

Η ίδια η καταχώριση φέρει δύο περιορισμούς — ένα ελάχιστο όριο PDF 1.5 για ενσωματωμένο χειρισμό Unicode και τα bits άδειας ενσωμάτωσης (embedding-permission bits) της γραμματοσειράς — αμφότερα καλύπτονται μαζί με τα βήματα εγκατάστασης στο άρθρο αναφοράς του RtLTextOut. Αυτό που ανήκει εδώ είναι η συνήθεια ελέγχου (audit habit): Το GetUnicodeGlyphForCodepoint είναι το σύστημα έγκαιρης προειδοποίησής σας. Διατρέξτε τα εύρη (ranges) codepoint που χρησιμοποιούν πραγματικά τα δεδομένα σας όταν ξεκινά η υπηρεσία και καταγράψτε ποια ID γλυφικών επιστρέφονται. Ένα κενό κάλυψης εμφανίζεται τότε ως μια γραμμή σε ένα αρχείο καταγραφής εκκίνησης (startup log) κατά τη διάρκεια του rollout, αντί για χαρακτήρες που λείπουν σε ένα τιμολόγιο που έφτασε ήδη σε έναν πελάτη

Η σειρά ανάγνωσης ανήκει στο έγγραφο, όχι στα γλυφικά

Το να έχετε κάθε γλυφικό σωστό εξακολουθεί να αφήνει ένα πράγμα ημιτελές. Το ISO 32000-1 §12.2 ορίζει μια προτίμηση προβολέα (viewer preference) που ονομάζεται /Direction (κατεύθυνση) η οποία δηλώνει τη συνολική σειρά ανάγνωσης του εγγράφου. Δεν αγγίζει κανένα γλυφικό. Αυτό που κάνει είναι να λέει σε έναν viewer πώς να τακτοποιεί τα δισέλιδα (two-up spreads), από ποια πλευρά πρέπει να ξεκινά μια διάταξη αντικριστών σελίδων (facing-page layout) και προς ποια κατεύθυνση θα πρέπει να κλίνει το UI ανάγνωσης. Τίποτα από αυτά δεν φαίνεται σε μια μεμονωμένη σελίδα, γι' αυτό ακριβώς και ξεχνιέται

// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft;  // adds vpDirection to ViewerPreferences

Η ρύθμιση του Direction είναι όλη η δουλειά: ο ορισμός ιδιότητας (property setter) προσθέτει το vpDirection στα ViewerPreferences του εγγράφου, έτσι μια γραμμή μεταφέρει την προτίμηση στο αρχείο. Εάν το κείμενο εξάγεται μέσω του RtLTextOut, το λαμβάνετε αυτό δωρεάν, επειδή η κλήση αναστρέφει την κατεύθυνση του εγγράφου ως παρενέργεια — το άρθρο αναφοράς καλύπτει το πότε ένα μικτό έγγραφο χρειάζεται να το αναιρέσει αυτό. Η περίπτωση όπου πρέπει να το ορίσετε εσείς οι ίδιοι είναι ένα έγγραφο δεξιά-προς-αριστερά που παράγεται με οποιονδήποτε άλλο τρόπο, για παράδειγμα από είσοδο που διαμορφώσατε εκ των προτέρων νωρίτερα στη διαδικασία (upstream) και σχεδιάσατε μέσα από τη συνηθισμένη διαδρομή. Παραλείψτε το και το δοκίμιο μιας σελίδας που κοιτάτε φαίνεται πανομοιότυπο και στις δύο περιπτώσεις· στη συνέχεια, κάποιος τυπώνει ένα φυλλάδιο διπλής όψης (duplex), τα δισέλιδα βγαίνουν κατοπτρισμένα, και η αιτία είναι μια γραμμή κώδικα (one-liner) που έλειπε από εβδομάδες νωρίτερα

Επαλήθευση της διαμορφωμένης εξόδου

Επαληθεύστε από άκρη σε άκρη (end to end), επειδή μια σελίδα μπορεί να φαίνεται σωστή και να εξακολουθεί να είναι άχρηστη για οτιδήποτε έπεται (downstream). Τρεις έλεγχοι βρίσκουν τα περισσότερα προβλήματα. Αντιγράψτε ξανά το κείμενο έξω από το Acrobat και συγκρίνετε τα codepoints με τη συμβολοσειρά πηγής (source string) σας. Εκτελέστε την αναζήτηση εντός του εγγράφου του viewer για μια λέξη που μπορείτε να δείτε στη σελίδα. Και ανοίξτε την έξοδο σε ένα μηχάνημα που δεν διαθέτει τις γραμματοσειρές ανάπτυξής (development fonts) σας, αυτό δηλαδή που είναι πιο πιθανό να εκθέσει μια αντικατάσταση. Τίποτα από αυτά δεν αντικαθιστά έναν φυσικό ομιλητή που κοιτάζει ένα πραγματικό έγγραφο, ο οποίος θα πιάσει πράγματα που κανένα συνθετικό σώμα κειμένου (synthetic corpus) δεν θα καταφέρει. Βάλτε αυτήν την αναθεώρηση στο ημερολόγιο προτού κυκλοφορήσει η μορφή (format)

Επιλέξτε συμβολοσειρές δοκιμής σκόπιμα αντί να ανακυκλώνετε ό,τι έστειλε ένας μεταφραστής πέρυσι. Ένα λειτουργικό ελάχιστο ανά τοπική ρύθμιση: μια πρόταση με καθαρή γραφή (pure-script), μια πρόταση με ενσωματωμένα λατινικά ονόματα επωνυμιών (brand names), μια γραμμή που φέρει ψηφία και νομίσματα, και ονόματα με διακριτικά (diacritics) ή σημάδια συνδυασμού. Τα πραγματικά ονόματα πελατών καταρρίπτουν τις υποθέσεις που το κείμενο-γέμισμα (filler text) αφήνει άθικτες, οπότε αφήστε το σύνολο παλινδρόμησης (regression set) να μεγαλώνει κατά μία συμβολοσειρά κάθε φορά που μια υπόθεση υποστήριξης (support case) αποκαλύπτει ένα μοτίβο που δεν είχατε δει

Η καταχώριση γραμματοσειρών, η δημιουργία υποσυνόλων (subsetting) και το καθημερινό API σχεδίασης κειμένου (text-drawing API) καλύπτονται στο άρθρο σχετικά με την έξοδο αναφοράς, τις γραμματοσειρές και τις εικόνες με το HotPDF. Όταν τα ίδια έγγραφα πρέπει επίσης να πληρούν τα προφίλ προσβασιμότητας, η προσθήκη ετικετών γλώσσας (language tagging) και οι κανόνες δομής στο άρθρο για την επικύρωση των PDF/A και PDF/UA μπαίνουν πάνω από την εργασία διαμόρφωσης (shaping) που περιγράφεται εδώ

Τα API από δεξιά προς τα αριστερά και γραμματοσειρών Unicode που περιγράφονται παραπάνω αποστέλλονται με το HotPDF Component για Delphi και C++Builder· η σελίδα του προϊόντος συνδέεται με την πλήρη αναφορά εξόδου κειμένου