Ένας τυφλός χρήστης ανοίγει μια τριμηνιαία αναφορά στο ολοκαίνουργιο viewer σας σε Delphi, ενεργοποιεί το NVDA, και ακούει το υποσέλιδο της σελίδας, μετά μια στήλη αριθμών, μετά τον τίτλο που κάθε βλέπων αναγνώστης θα είχε διαβάσει πρώτος. Ή δεν ακούει τίποτα καθόλου. Η σελίδα φαίνεται τέλεια στην οθόνη, και αυτή είναι ακριβώς η παγίδα: η απόδοση και η ανάγνωση είναι διαφορετικά προβλήματα που λύνονται από διαφορετικό κώδικα. Η σειρά με την οποία ένα PDF ζωγραφίζει τα γράμματά του δεν έχει καμία υποχρέωση να ταιριάζει με τη σειρά με την οποία θα έπρεπε να τα ακούσει ένα άτομο, οπότε ένας viewer χτισμένος μόνο πάνω σε κλήσεις απόδοσης παράγει μια άψογη εικόνα και μια αχρησιμοποίητη αφήγηση. Το PDFium Component, το VCL/LCL wrapper γύρω από τη μηχανή PDFium για Delphi, C++Builder και Lazarus, φέρει ένα ξεχωριστό σύνολο APIs ανάγνωσης γι' αυτόν τον λόγο. Τα APIs σχεδίασης δεν μπορούν να ανακτήσουν μια σειρά ανάγνωσης που ποτέ δεν τους δόθηκε
Ένας προσβάσιμος reader στέκεται ή πέφτει πάνω σε τρία πράγματα. Πρέπει να εξάγει μια σειρά που μπορεί να εκφωνήσει ένας screen reader, να κρατά έναν ορατό δρομέα λέξης καρφωμένο σε ό,τι λέει η φωνή, και να παραδέχεται όταν ένα έγγραφο ποτέ δεν έχει επισημανθεί με tags αντί να μαντεύει και να προσποιείται. Το καθένα έχει ένα σαφές API στο οποίο να απευθυνθείτε και μια αποτυχία που δαγκώνει αν παραλείψετε τη λεπτομέρεια
Η σειρά ανάγνωσης ζει στο δέντρο δομής, όχι στη σειρά σχεδίασης
Το ISO 32000-1 §14.8 ορίζει τη λογική δομή ως ένα δέντρο στοιχείων στρωματοποιημένο πάνω από το περιεχόμενο της σελίδας. Το PDF/UA (ISO 14289-1) πηγαίνει παραπέρα και κάνει αυτό το δέντρο υποχρεωτικό: κάθε κομμάτι πραγματικού περιεχομένου πρέπει να είναι προσβάσιμο μέσα από αυτό με σειρά ανάγνωσης, με τα artifacts σελίδας σημειωμένα ως τέτοια και παραλειπόμενα. Μια σωστά επισημασμένη με tags αναφορά ξέρει ότι το «Quarterly Results» είναι μια επικεφαλίδα επιπέδου δύο και ότι το πλέγμα συνόλων είναι ένας πίνακας με κελιά κεφαλίδας. Μια αναφορά χωρίς tags είναι ένας σωρός τοποθετημένων ακολουθιών γραμμάτων που τυχαίνει να μοιάζουν με έγγραφο
Η ReadablePageContent διατρέχει αυτή τη δομή όταν υπάρχει και επιστρέφει θραύσματα επισημασμένα με ένα σημασιολογικό Kind, τιμές όπως cfHeading και cfParagraph, ώστε το UI να μπορεί να πει «επικεφαλίδα» πριν από τις λέξεις αντί να διαβάζει μια γραμμή με έντονα γράμματα σαν συνηθισμένο κείμενο σώματος. Χωρίς χρησιμοποιήσιμο δέντρο, η ίδια κλήση καταφεύγει σε ευρετική ανάλυση διάταξης: εντοπίζει στήλες, ομαδοποιεί γραμμές βάσης, ταξινομεί από αριστερά προς τα δεξιά και από πάνω προς τα κάτω. Αυτή η εναλλακτική λειτουργεί καλά για ένα σημείωμα μίας στήλης και είναι επισφαλής για ένα newsletter, μια φόρμα πολλών στηλών, οτιδήποτε έχει πλαϊνή στήλη ή απόσπασμα-παράθεση. Αυτό που έχει σημασία είναι να γνωρίζετε ποιο αποτέλεσμα πήρατε, και το API σας το λέει ανοιχτά. Η εγγραφή TPdfReadableContent φέρει ένα πεδίο Source ορισμένο σε rosStructure όταν η σειρά προήλθε από το δέντρο με tags, ή σε rosHeuristic όταν συνήχθη από τη γεωμετρία. Δείξτε μια μαντεμένη σειρά σαν να ήταν επαληθευμένη και έχετε παραδώσει την εκδοχή προσβασιμότητας ενός σήματος επιτυχίας πάνω σε ένα build που ποτέ δεν έτρεξε κανείς
Η φθηνή κίνηση κατά το άνοιγμα είναι να διαβάσετε την IsTagged και να καλέσετε την ValidatePdfUa μία φορά, και μετά να κρατήσετε την απάντηση σε cache. Ένας αποτυχημένος έλεγχος PDF/UA δεν είναι λόγος να απορρίψετε το αρχείο. Είναι λόγος να βάλετε «εκτιμώμενη σειρά ανάγνωσης» στη γραμμή κατάστασης, ώστε όταν ένας πελάτης στείλει παράπονο για ακατάληπτη αφήγηση, η υποστήριξη να ξέρει ήδη αν αντιμετωπίζει πρόβλημα tagging στο αρχείο ή σφάλμα στον κώδικά σας
Από τη σελίδα στην ουρά ομιλίας με το ReadingUnits
Για text-to-speech, η ReadingUnits κάνει τη βαριά δουλειά. Επιστρέφει έναν πίνακα εγγραφών TPdfReadingUnit για την ενεργή σελίδα, καθεμία κρατώντας το κείμενο προς εκφώνηση, τον σημασιολογικό ρόλο του, και τα ορθογώνια που το εντοπίζουν στη σελίδα. Υπάρχει ένας σύντροφος σε επίπεδο εγγράφου, η DocumentReadingUnits, όταν θέλετε συνεχή ανάγνωση σε πολλές σελίδες. Μία μονάδα πέφτει κατευθείαν σε μία θέση μιας ουράς ομιλίας:
procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
Units: TPdfReadingUnits;
i: Integer;
begin
Pdf.PageNumber := PageNumber; // η ReadingUnits δουλεύει πάνω στην ενεργή σελίδα
Units := Pdf.ReadingUnits;
FSpeechQueue.Clear;
for i := Low(Units) to High(Units) do
FSpeechQueue.Add(Units[i]); // κείμενο + σημασιολογία + ορθογώνια επισήμανσης
FCurrentPage := PageNumber;
SpeakNextUnit;
end;
Δύο πράγματα σε αυτόν τον βρόχο είναι εύκολο να τα κάνετε λάθος. Κρατήστε την ουρά ανά σελίδα και ξαναχτίστε τη κάθε φορά που ο χρήστης πλοηγείται, επειδή οι μονάδες ανάγνωσης φέρουν ορθογώνια σε χώρο σελίδας· μια ουρά που έχει απομείνει από τη σελίδα τρία θα ζωγραφίσει τις επισημάνσεις της πάνω στη σελίδα τέσσερα. Και αντιμετωπίστε έναν άδειο πίνακα Units σε μια σελίδα που ξεκάθαρα έχει περιεχόμενο ως τον ανιχνευτή σας μόνο-εικόνας. Μια σαρωμένη σελίδα είναι εικονοστοιχεία χωρίς επίπεδο κειμένου από κάτω, και η σωστή απάντηση είναι να εκφωνήσετε μια προειδοποίηση («αυτή η σελίδα δεν έχει εξαγώγιμο κείμενο») αντί να σιωπήσετε με τρόπο που ο ακροατής δεν μπορεί να ξεχωρίσει από ένα κρέμασμα
Ένας δρομέας λέξης που ακολουθεί τη φωνή
Η επισήμανση ολόκληρης παραγράφου τη φορά φαίνεται αργή σε έναν χρήστη με μειωμένη όραση που παρακολουθεί τις λέξεις με το μάτι ενώ διαβάζονται δυνατά. Η επισήμανση σε επίπεδο λέξης, το εφέ karaoke, χρειάζεται δύο κομμάτια: τη γεωμετρία κάθε λέξης, και έναν τρόπο να αντιστοιχίσετε τις αναφορές προόδου της μηχανής TTS σε αυτή τη γεωμετρία. Η PageWordBoxes σας δίνει τη γεωμετρία ως εγγραφές TPdfWordBox, καθεμία με το κείμενο της λέξης, τη μετατόπιση χαρακτήρα της, το πλήθος χαρακτήρων της, και ένα ορθογώνιο σε χώρο σελίδας. Η TrackReadingWordAt σας δίνει την αντιστοίχιση. Δώστε της τη θέση χαρακτήρα που ήδη αναφέρει το συμβάν ορίου λέξης του SAPI, και αυτή επιλύει εκείνη τη μετατόπιση σε έναν δείκτη μέσα στον πίνακα word-box και ζωγραφίζει τον δρομέα πάνω στη λέξη που ταιριάζει σε μία κλήση
procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
// Τα word boxes της view προέρχονται από τη σελίδα που εμφανίζει η view.
// Ο ορισμός μόνο του Pdf.PageNumber δεν θα μετακινούσε τη view
PdfView.PageNumber := PageNumber;
FWordBoxes := PdfView.PageWordBoxes;
end;
procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
WordIdx: Integer;
begin
// Η TrackReadingWordAt αντιστοιχίζει τη μετατόπιση ΚΑΙ ζωγραφίζει τον δρομέα λέξης
WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
if WordIdx < 0 then
PdfView.ClearReadingWord; // το όριο ξεπέρασε το κείμενο της σελίδας
end;
Το συμβόλαιο είναι γενναιόδωρο σε ένα σημείο και ανελέητο σε άλλο. Το γενναιόδωρο μέρος: η TrackReadingWordAt κρατά τη δική της cache word-box για τη σελίδα που παρακολουθεί, οπότε δεν υπάρχει τίποτα να προφορτωθεί, και δεν συμβαίνει καθόλου απόδοση επειδή τα word boxes προέρχονται από το επίπεδο κειμένου. Μια υπηρεσία ομιλίας χωρίς κεφαλή, χωρίς ορατό παράθυρο, μπορεί ακόμα να παρακολουθεί θέσεις. Το ανελέητο μέρος: ο δείκτης χαρακτήρα πρέπει να δείχνει μέσα στο κείμενο που εξήγαγε το component, όχι μέσα σε κάποια καθαρισμένη συμβολοσειρά που χτίσατε εσείς οι ίδιοι. Όταν το CharIndex ξεπερνά το τέλος του κειμένου της σελίδας, η συνάρτηση επιστρέφει -1 αντί να εγείρει σφάλμα, κάτι που συμβαίνει συνέχεια όταν μια μηχανή TTS πυροδοτεί ένα τελευταίο συμβάν ορίου για ένα σημείο στίξης στο τέλος. Διαβάστε το -1 ως «καθαρίστε τον δρομέα», ποτέ ως σφάλμα
Στην πλευρά εμφάνισης, η ReadingWordColor ορίζει το χρώμα του δρομέα. Το προεπιλεγμένο κεχριμπαρί κρατά καλά πάνω από τα περισσότερα φόντα σελίδας, αλλά δοκιμάστε το κάτω από κάθε φίλτρο εμφάνισης που προσφέρει ο viewer σας. Ένας κεχριμπαρένιος δρομέας μπορεί να εξαφανιστεί εντελώς κάτω από αντιστροφή χρωμάτων, και η αντιστροφή που τρέχει παράλληλα με την ομιλία είναι ακριβώς ο τρόπος που δουλεύει ένας χρήστης με μειωμένη όραση, οπότε ο ένας συνδυασμός που πρέπει περισσότερο να πετύχετε είναι αυτός που ένα γρήγορο demo ποτέ δεν εξασκεί. Ορίστε την ReadingWordFollow σε True και η view κυλά τη λέξη που εκφωνείται μέσα στο οπτικό πεδίο από μόνη της, κάτι που δεν μπορείτε να κάνετε χωρίς αυτό σε μια μεγεθυσμένη σελίδα που ξεχειλίζει σε πολλές οθόνες. Προσέξτε έναν κανόνα εμβέλειας: η SetReadingWord ζωγραφίζει μόνο στην ενεργή σελίδα του TPdfView. Αποφασίστε εκ των προτέρων αν η χειροκίνητη κύλιση παύει την ομιλία ή αν η συμπεριφορά παρακολούθησης υπερισχύει, επειδή το να μην επιλέξετε κανένα από τα δύο αφήνει τη φωνή να διαβάζει ενώ ο δρομέας κάθεται κάπου εκτός οθόνης
Τα έγγραφα που σπάνε τον reader σας
Μια χούφτα σχημάτων εισόδου νικούν μια αφελή υλοποίηση αρκετά αξιόπιστα ώστε να ανήκουν ως μόνιμα δείγματα στη σουίτα παλινδρόμησης, όχι ως εφάπαξ σφάλματα που διορθώνετε και ξεχνάτε
- Αρχεία χωρίς tags αλλά πλούσια σε κείμενο. Η ευρετική σειρά τείνει να είναι σωστή για μια γραμμική αναφορά και λάθος τη στιγμή που μπαίνει μια πλαϊνή στήλη ή απόσπασμα-παράθεση. Σημειώστε τη σειρά ως εκτιμώμενη, τόσο στο UI όσο και στο αρχείο διαγνωστικών σας, ώστε η αποτυχία να είναι ευανάγνωστη αργότερα
- Σαρώσεις μόνο-εικόνας. Κανένα επίπεδο κειμένου καθόλου. Εντοπίστε τις μέσω άδειων μονάδων ανάγνωσης και κατευθύνετε τον χρήστη σε ένα βήμα OCR νωρίτερα στη ροή αντί να αφήσετε τον reader να αφηγηθεί μια άδεια σελίδα
- Συνδυαστικοί χαρακτήρες και μεικτά συστήματα γραφής. Οι συνδυαστικοί χαρακτήρες Unicode δεν συμπτύσσονται πάντα ένα προς ένα σε οπτικές λέξεις, οπότε ο αριθμός word-box μπορεί να αποκλίνει από ό,τι περιμένει ο δικός σας tokenizer. Μην ευρετηριάζετε τον πίνακα word-box με μετατοπίσεις που υπολογίσατε διαχωρίζοντας το κείμενο μόνοι σας· χρησιμοποιήστε μόνο τους δείκτες που επιστρέφει η
TrackReadingWordAt
Δοκιμάστε το σαν ελεγκτής, όχι σαν demo
«Διάβασε το δείγμα μου δυνατά» δεν αποδεικνύει τίποτα. Μια επιτυχία που μπορείτε να υπερασπιστείτε περνά τρία αρχεία μέσα από το τελικό build με συνδεδεμένο το NVDA: ένα γνωστό αρχείο με tags, όπου οι επικεφαλίδες ανακοινώνονται ως επικεφαλίδες και ένας πίνακας διαβάζεται με σειρά γραμμών· ένα γνωστό αρχείο χωρίς tags, όπου ο δείκτης εκτιμώμενης σειράς είναι ορατός· και μια σάρωση, όπου η προειδοποίηση χωρίς-κείμενο πραγματικά εκφωνείται. Καθένα εξασκεί μια διαδρομή που η ευτυχής περίπτωση παραλείπει
Από εκεί, επιβεβαιώστε ότι ο δρομέας λέξης παραμένει κλειδωμένος στον διπλάσιο ρυθμό ομιλίας και στον μισό, και ότι η κύλιση της ReadingWordFollow δεν παλεύει με τη δική του κύλιση του χρήστη. Έπειτα τρέξτε ομιλία ενώ κάνετε κύκλο σε κάθε φίλτρο χρώματος και παρακολουθήστε ότι ο δρομέας ποτέ δεν εξαφανίζεται. Το άρθρο για τα φίλτρα χρώματος για μειωμένη όραση καλύπτει αυτή τη διαδρομή απόδοσης λεπτομερώς, και η βαθιά ανάλυση του δρομέα ομιλίας λέξης ξεδιαλύνει τον χρονισμό TTS
Τα APIs reading-unit και word-box που χρησιμοποιήθηκαν παραπάνω συνοδεύουν το PDFium Component για Delphi και C++Builder (VCL) και Lazarus/FPC (LCL). Η σελίδα προϊόντος συνδέει την πλήρη τεκμηρίωση API, συμπεριλαμβανομένων των διατάξεων εγγραφών για τις μονάδες ανάγνωσης και τα word boxes πίσω από αυτά τα παραδείγματα