Un utilizator nevăzător deschide un raport trimestrial în noul dvs. viewer Delphi strălucitor, pornește NVDA și aude subsolul paginii, apoi o coloană de cifre, apoi titlul pe care orice cititor văzător l-ar fi citit primul. Sau nu aude nimic deloc. Pagina arată perfect pe ecran, și exact aceasta este capcana: randarea și citirea sunt probleme diferite, rezolvate de cod diferit. Ordinea în care un PDF își pictează glifele nu are nicio obligație să corespundă ordinii în care o persoană ar trebui să le audă, așa că un viewer construit doar pe apeluri de randare produce o imagine impecabilă și o narațiune inutilizabilă. PDFium Component, wrapper-ul VCL/LCL în jurul motorului PDFium pentru Delphi, C++Builder și Lazarus, poartă un set separat de API-uri de citire din acest motiv. API-urile de desenare nu pot recupera o ordine de citire care nu le-a fost oferită niciodată
Un cititor accesibil se ridică sau cade pe baza a trei lucruri. Trebuie să extragă o ordine pe care un cititor de ecran o poate rosti, să țină un cursor de cuvânt vizibil fixat pe orice spune vocea și să recunoască atunci când un document nu a fost niciodată etichetat, în loc să ghicească și să se prefacă. Fiecare are un API clar la care să apelați și un eșec care mușcă dacă săriți detaliul
Ordinea de citire trăiește în arborele de structură, nu în ordinea de pictare
ISO 32000-1 §14.8 definește structura logică drept un arbore de elemente stratificat peste conținutul paginii. PDF/UA (ISO 14289-1) merge mai departe și face acel arbore obligatoriu: fiecare bucată de conținut real trebuie să fie accesibilă prin el în ordinea de citire, cu artefactele de pagină marcate ca atare și omise. Un raport etichetat corect știe că „Quarterly Results” este un titlu de nivel doi și că grila de totaluri este un tabel cu celule de antet. Un raport neetichetat este o grămadă de secvențe de glife poziționate care se întâmplă să arate ca un document
ReadablePageContent parcurge acea structură atunci când este prezentă și returnează fragmente etichetate cu un Kind semantic, valori precum cfHeading și cfParagraph, astfel încât UI-ul poate spune „titlu” înainte de cuvinte, în loc să citească o linie îngroșată ca text obișnuit de corp. Fără un arbore utilizabil, același apel revine la analiza euristică de aspect: detectează coloane, grupează liniile de bază, ordonează de la stânga la dreapta și de sus în jos. Acea rezervă funcționează bine pentru un memo cu o singură coloană și e șubredă pentru un buletin informativ, un formular pe mai multe coloane, orice are o bară laterală sau un citat evidențiat. Ce contează este să știți ce rezultat ați obținut, iar API-ul vă spune direct. Înregistrarea TPdfReadableContent poartă un câmp Source setat la rosStructure atunci când ordinea a venit din arborele etichetat, sau rosHeuristic atunci când a fost dedusă din geometrie. Arătați o ordine ghicită ca și cum ar fi verificată și ați livrat versiunea de accesibilitate a unei insigne de reușită pe un build pe care nimeni nu l-a rulat
Mișcarea ieftină la momentul deschiderii este să citiți IsTagged și să apelați ValidatePdfUa o singură dată, apoi să puneți răspunsul în cache. O verificare PDF/UA eșuată nu este motiv pentru a refuza fișierul. Este motiv să puneți „ordine de citire estimată” în bara de stare, astfel încât atunci când un client trimite o reclamație despre o narațiune încâlcită, suportul știe deja dacă se uită la o problemă de etichetare din fișier sau la un bug din codul dumneavoastră
De la pagină la coada de voce cu ReadingUnits
Pentru text-to-speech, ReadingUnits face munca grea. Returnează un array de înregistrări TPdfReadingUnit pentru pagina activă, fiecare purtând textul de rostit, rolul său semantic și dreptunghiurile care îl localizează pe pagină. Există un însoțitor la nivel de document, DocumentReadingUnits, pentru când vreți citire continuă pe mai multe pagini. O unitate se potrivește direct într-un slot al unei cozi de voce:
procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
Units: TPdfReadingUnits;
i: Integer;
begin
Pdf.PageNumber := PageNumber; // ReadingUnits lucrează pe pagina activă
Units := Pdf.ReadingUnits;
FSpeechQueue.Clear;
for i := Low(Units) to High(Units) do
FSpeechQueue.Add(Units[i]); // text + semantică + dreptunghiuri de evidențiere
FCurrentPage := PageNumber;
SpeakNextUnit;
end;
Două lucruri din acea buclă sunt ușor de greșit. Țineți coada per pagină și reconstruiți-o de fiecare dată când utilizatorul navighează, pentru că unitățile de citire poartă dreptunghiuri în spațiul paginii; o coadă rămasă de la pagina trei își va picta evidențierile pe pagina patru. Și tratați un array Units gol pe o pagină care are clar conținut ca detectorul dvs. de scanare-doar-imagine. O pagină scanată este pixeli fără niciun strat de text dedesubt, iar răspunsul corect este să rostiți un avertisment („această pagină nu are text extractibil”), nu să tăceți într-un fel pe care ascultătorul nu îl poate deosebi de o blocare
Un cursor de cuvânt care urmărește vocea
Evidențierea unui întreg paragraf odată se simte greoaie pentru un utilizator cu vedere slabă care urmărește cuvintele cu ochiul în timp ce sunt citite cu voce tare. Evidențierea la nivel de cuvânt, efectul de tip karaoke, are nevoie de două piese: geometria fiecărui cuvânt, și un mod de a mapa rapoartele de progres ale motorului TTS pe acea geometrie. PageWordBoxes vă oferă geometria ca înregistrări TPdfWordBox, fiecare cu textul cuvântului, offsetul său de caracter, numărul său de caractere și un dreptunghi în spațiul paginii. TrackReadingWordAt vă oferă maparea. Alimentați-l cu poziția de caracter pe care evenimentul de graniță-de-cuvânt al SAPI o raportează deja, iar el rezolvă acel offset la un index în array-ul de casete-de-cuvânt și pictează cursorul pe cuvântul care corespunde, într-un singur apel
procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
// Casetele de cuvânt ale view-ului provin din pagina pe care view-ul o afișează.
// Setarea doar a Pdf.PageNumber nu ar mișca view-ul
PdfView.PageNumber := PageNumber;
FWordBoxes := PdfView.PageWordBoxes;
end;
procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
WordIdx: Integer;
begin
// TrackReadingWordAt mapează offsetul ȘI pictează cursorul de cuvânt
WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
if WordIdx < 0 then
PdfView.ClearReadingWord; // granița a trecut de textul paginii
end;
Contractul este generos pe un punct și neiertător pe altul. Partea generoasă: TrackReadingWordAt își ține propriul cache de casete-de-cuvânt pentru pagina pe care o urmărește, așa că nu este nimic de pre-încărcat, și nu se întâmplă nicio randare, pentru că indicatoarele de cuvânt provin din stratul de text. Un serviciu de voce headless, fără nicio fereastră vizibilă, poate încă urmări pozițiile. Partea neiertătoare: indexul de caracter trebuie să indice în textul pe care componenta l-a extras, nu într-un șir curățat pe care l-ați construit chiar dvs. Când CharIndex depășește sfârșitul textului paginii, funcția returnează -1 în loc să ridice o excepție, ceea ce se întâmplă tot timpul când un motor TTS declanșează un ultim eveniment de graniță pentru punctuația finală. Citiți -1 ca „ștergeți cursorul”, niciodată ca o eroare
Pe partea de afișare, ReadingWordColor setează culoarea cursorului. Chihlimbarul implicit rezistă pe majoritatea fundalurilor de pagină, dar testați-l sub fiecare filtru de afișare pe care viewer-ul dvs. îl oferă. Un cursor chihlimbariu poate dispărea complet sub inversarea culorilor, iar inversarea care rulează alături de voce este exact modul în care lucrează un utilizator cu vedere slabă, așa că singura combinație pe care trebuie neapărat să o faceți bine este exact cea pe care o demonstrație rapidă nu o exersează niciodată. Setați ReadingWordFollow pe True și view-ul derulează singur cuvântul rostit în vizor, ceea ce nu vă puteți permite să omiteți pe o pagină zoomată care se întinde peste mai multe ecrane. Țineți cont de o regulă de domeniu: SetReadingWord pictează doar pe pagina activă a TPdfView. Decideți din start dacă derularea manuală pune vocea pe pauză sau dacă comportamentul de urmărire o suprascrie, pentru că a nu alege niciuna lasă vocea să citească în continuare, în timp ce cursorul stă undeva în afara ecranului
Documentele care vă rup cititorul
O mână de forme de intrare învinge o implementare naivă suficient de fiabil încât merită să fie eșantioane permanente în suita de regresie, nu bug-uri de o singură dată, pe care le reparați și le uitați
- Fișiere neetichetate, dar bogate în text. Ordinea euristică tinde să fie corectă pentru un raport liniar și greșită în momentul în care intervine o bară laterală sau un citat evidențiat. Marcați ordinea drept estimată, atât în UI, cât și în jurnalul dvs. de diagnostic, astfel încât eșecul să fie lizibil mai târziu
- Scanări doar-imagine. Niciun strat de text. Prindeți-le prin unități de citire goale și îndreptați utilizatorul spre un pas OCR în amonte, în loc să lăsați cititorul să narreze o pagină goală
- Caractere combinatoare și scripturi mixte. Semnele combinatoare Unicode nu se colapsează întotdeauna unu-la-unu în cuvinte vizuale, așa că numărul de casete-de-cuvânt poate devia de la ce așteaptă propriul dvs. tokenizator. Nu indexați array-ul de casete-de-cuvânt cu offset-uri calculate de dvs. prin împărțirea textului; folosiți doar indicii pe care îi returnează
TrackReadingWordAt
Testați-l ca un auditor, nu ca o demonstrație
„A citit eșantionul meu cu voce tare” nu dovedește nimic. O trecere pe care o puteți apăra rulează trei fișiere prin build-ul finit cu NVDA atașat: un fișier cunoscut ca etichetat, unde titlurile sunt anunțate ca titluri și un tabel este citit în ordinea rândurilor; un fișier cunoscut ca neetichetat, unde indicatorul de ordine estimată este vizibil; și o scanare, unde avertismentul de lipsă-a-textului chiar este rostit. Fiecare exersează o cale pe care cazul fericit o sare
De acolo, confirmați că cursorul de cuvânt rămâne blocat la dublul vitezei de vorbire și la jumătate, și că derularea ReadingWordFollow nu se luptă cu derularea proprie a utilizatorului. Apoi rulați vocea în timp ce parcurgeți fiecare filtru de culoare și urmăriți ca cursorul să nu dispară niciodată. Articolul despre filtrele de culoare pentru vedere slabă acoperă în detaliu acea cale de randare, iar analiza aprofundată a cursorului de vorbire pe cuvânt desface temporizarea TTS
API-urile de unități de citire și casete-de-cuvânt folosite mai sus sunt livrate cu PDFium Component pentru Delphi și C++Builder (VCL) și Lazarus/FPC (LCL). Pagina de produs leagă către referința API completă, inclusiv structurile de înregistrare pentru unitățile de citire și casetele de cuvânt din spatele acestor exemple