Nevidiaci používateľ otvorí štvrťročnú správu vo vašom novučkom zobrazovači v Delphi, zapne NVDA a počuje pätu strany, potom stĺpec čísel a až potom titulok, ktorý by vidiaci čitateľ prečítal ako prvý. Alebo nepočuje vôbec nič. Strana vyzerá na obrazovke perfektne a presne v tom je pasca: vykresľovanie a čítanie sú rozdielne problémy riešené rozdielnym kódom. Poradie, v akom PDF kreslí svoje glyfy, nemá žiadnu povinnosť zhodovať sa s poradím, v akom by ich mal človek počuť, takže zobrazovač postavený len na vykresľovacích volaniach vyprodukuje bezchybný obraz a nepoužiteľné rozprávanie. PDFium Component, obal VCL/LCL nad enginom PDFium pre Delphi, C++Builder a Lazarus, práve preto nesie samostatnú sadu rozhraní na čítanie. Kresliace rozhrania nedokážu obnoviť poradie čítania, ktoré nikdy nedostali
Prístupná čítačka stojí a padá na troch veciach. Musí vytiahnuť poradie, ktoré dokáže čítačka obrazovky vysloviť, udržať viditeľný kurzor slova prišpendlený k tomu, čo hlas práve hovorí, a priznať, že dokument nikdy nebol otagovaný, namiesto hádania a predstierania. Ku každej z nich vedie jasné rozhranie a každá má zlyhanie, ktoré uštipne, ak detail preskočíte
Poradie čítania žije v strome štruktúry, nie v poradí kreslenia
ISO 32000-1 §14.8 definuje logickú štruktúru ako strom prvkov navrstvený nad obsahom strany. PDF/UA (ISO 14289-1) ide ďalej a tento strom robí povinným: každý kus skutočného obsahu musí byť cezeň dosiahnuteľný v poradí čítania, pričom artefakty strany sú ako také označené a preskočené. Správne otagovaná správa vie, že „Quarterly Results“ je nadpis druhej úrovne a že mriežka súčtov je tabuľka s bunkami hlavičky. Neotagovaná správa je kopa umiestnených behov glyfov, ktoré len náhodou vyzerajú ako dokument
ReadablePageContent túto štruktúru prejde, ak je prítomná, a vráti fragmenty označené sémantickým poľom Kind, hodnotami ako cfHeading a cfParagraph, takže rozhranie môže pred slovami povedať „nadpis“ namiesto toho, aby tučný riadok prečítalo ako obyčajný text. Bez použiteľného stromu sa to isté volanie vráti k heuristickej analýze rozloženia: rozpozná stĺpce, zhlukne účiarie a usporiada zľava doprava a zhora nadol. Toto záložné riešenie je v poriadku pre jednostĺpcové oznámenie a vratké pre newsletter, viacstĺpcový formulár alebo čokoľvek s bočným panelom či vytiahnutým citátom. Podstatné je vedieť, ktorý výsledok ste dostali, a rozhranie vám to povie priamo. Záznam TPdfReadableContent nesie pole Source nastavené na rosStructure, keď poradie prišlo z otagovaného stromu, alebo na rosHeuristic, keď bolo odvodené z geometrie. Ukážte odhadnuté poradie, akoby bolo overené, a dodali ste prístupnostnú obdobu zeleného odznaku na builde, ktorý nikto nespustil
Lacným ťahom pri otvorení je prečítať IsTagged, raz zavolať ValidatePdfUa a odpoveď si odložiť do vyrovnávacej pamäte. Neúspešná kontrola PDF/UA nie je dôvodom súbor odmietnuť. Je dôvodom napísať do stavového riadka „odhadnuté poradie čítania“, aby podpora pri sťažnosti na popletené rozprávanie hneď vedela, či sa pozerá na problém s tagovaním v súbore, alebo na chybu vo vašom kóde
Od strany k frontu reči cez ReadingUnits
Pri prevode textu na reč odvedie ťažkú prácu ReadingUnits. Vráti pole záznamov TPdfReadingUnit pre aktívnu stranu, pričom každý drží text na vyslovenie, jeho sémantickú rolu a obdĺžniky, ktoré ho na strane lokalizujú. Existuje aj náprotivok pre celý dokument, DocumentReadingUnits, keď chcete čítať plynule cez strany. Jedna jednotka zapadne priamo do jedného slotu frontu reči:
procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
Units: TPdfReadingUnits;
i: Integer;
begin
Pdf.PageNumber := PageNumber; // ReadingUnits pracuje s aktívnou stranou
Units := Pdf.ReadingUnits;
FSpeechQueue.Clear;
for i := Low(Units) to High(Units) do
FSpeechQueue.Add(Units[i]); // text + sémantika + obdĺžniky zvýraznenia
FCurrentPage := PageNumber;
SpeakNextUnit;
end;
Dve veci sa v tomto cykle dajú ľahko pokaziť. Držte front na jednu stranu a prestavajte ho vždy, keď používateľ naviguje, pretože jednotky čítania nesú obdĺžniky v priestore strany; front, ktorý zostal po strane tri, vykreslí svoje zvýraznenia na stranu štyri. A prázdne pole Units na strane, ktorá zjavne obsah má, berte ako svoj detektor obrázkových strán. Naskenovaná strana sú pixely bez textovej vrstvy pod nimi a správnou odpoveďou je vysloviť varovanie („táto strana nemá vytiahnuteľný text“) namiesto toho, aby ste stíchli spôsobom, ktorý poslucháč nerozozná od zamrznutia
Kurzor slova, ktorý sleduje hlas
Zvýrazňovanie celého odseku naraz pôsobí ťarbavo na slabozrakého používateľa, ktorý slová sleduje očami, kým sa čítajú nahlas. Zvýrazňovanie na úrovni slov, teda karaoke efekt, potrebuje dva kusy: geometriu každého slova a spôsob, ako namapovať hlásenia o postupe z enginu TTS na túto geometriu. PageWordBoxes vám dá geometriu ako záznamy TPdfWordBox, každý s textom slova, jeho znakovým offsetom, počtom znakov a obdĺžnikom v priestore strany. TrackReadingWordAt vám dá mapovanie. Podajte mu pozíciu znaku, ktorú udalosť hranice slova v SAPI aj tak hlási, a on tento offset vyrieši na index v poli slovných rámčekov a jedným volaním vykreslí kurzor na zodpovedajúcom slove
procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
// Slovné rámčeky pohľadu pochádzajú zo strany, ktorú pohľad zobrazuje.
// Samotné nastavenie Pdf.PageNumber by pohľadom nepohlo
PdfView.PageNumber := PageNumber;
FWordBoxes := PdfView.PageWordBoxes;
end;
procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
WordIdx: Integer;
begin
// TrackReadingWordAt mapuje offset A ZÁROVEŇ vykreslí kurzor slova
WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
if WordIdx < 0 then
PdfView.ClearReadingWord; // hranica prebehla za text strany
end;
Kontrakt je v jednej veci veľkorysý a v druhej neúprosný. Veľkorysá časť: TrackReadingWordAt si pre stranu, ktorú sleduje, drží vlastnú vyrovnávaciu pamäť slovných rámčekov, takže nie je čo prednačítať, a nedochádza k žiadnemu vykresľovaniu, keďže slovné rámčeky pochádzajú z textovej vrstvy. Bezhlavá rečová služba bez viditeľného okna dokáže pozície sledovať tiež. Neúprosná časť: index znaku musí ukazovať do textu, ktorý komponent vytiahol, nie do nejakého vyčisteného reťazca, ktorý ste si postavili sami. Keď CharIndex prebehne za koniec textu strany, funkcia vráti -1 namiesto vyvolania výnimky, čo sa deje neustále, keď engine TTS vystrelí poslednú udalosť hranice pre koncovú interpunkciu. Hodnotu -1 čítajte ako „zmaž kurzor“, nikdy nie ako chybu
Na strane zobrazenia nastavuje farbu kurzora vlastnosť ReadingWordColor. Predvolená jantárová obstojí nad väčšinou pozadí strán, ale otestujte ju pod každým zobrazovacím filtrom, ktorý váš zobrazovač ponúka. Jantárový kurzor môže pri inverzii farieb zmiznúť úplne a inverzia bežiaca súbežne s rečou je presne to, ako slabozraký používateľ pracuje, takže jediná kombinácia, ktorú najviac potrebujete zvládnuť, je tá, ktorú rýchle demo nikdy nevyskúša. Nastavte ReadingWordFollow na True a pohľad si vyslovené slovo sám posunie do zorného poľa, bez čoho sa na priblíženej strane rozliatej cez obrazovky nezaobídete. Dajte pozor na jedno pravidlo rozsahu: SetReadingWord kreslí len na aktívnej strane objektu TPdfView. Rozhodnite sa dopredu, či ručné rolovanie reč pozastaví, alebo či ho správanie sledovania prebije, pretože ak si nevyberiete ani jedno, hlas číta ďalej, kým kurzor sedí kdesi mimo obrazovky
Dokumenty, ktoré vašu čítačku rozbijú
Hŕstka tvarov vstupu porazí naivnú implementáciu tak spoľahlivo, že patria do regresnej sady ako trvalé vzorky, nie ako jednorazové chyby, ktoré opravíte a zabudnete
- Neotagované, no textom bohaté súbory. Heuristické poradie býva správne pri lineárnej správe a nesprávne vo chvíli, keď vstúpi bočný panel alebo vytiahnutý citát. Označte poradie ako odhadnuté v rozhraní aj v diagnostickom logu, aby bolo zlyhanie neskôr čitateľné
- Čisto obrázkové skeny. Bez akejkoľvek textovej vrstvy. Zachyťte ich cez prázdne jednotky čítania a nasmerujte používateľa na krok OCR skôr v reťazci namiesto toho, aby čítačka rozprávala prázdnu stranu
- Kombinujúce znaky a zmiešané písma. Kombinujúce znamienka Unicode sa nie vždy skladajú jedna k jednej do vizuálnych slov, takže počet slovných rámčekov sa môže rozísť s tým, čo očakáva váš vlastný tokenizér. Neindexujte pole slovných rámčekov offsetmi, ktoré ste si vypočítali vlastným delením textu; používajte len indexy, ktoré vráti
TrackReadingWordAt
Testujte to ako audítor, nie ako predvádzač
„Moju vzorku to prečítalo nahlas“ nedokazuje nič. Obhájiteľný test prežene hotovým buildom s pripojeným NVDA tri súbory: jeden známy otagovaný, kde sa nadpisy ohlásia ako nadpisy a tabuľka sa číta po riadkoch; jeden známy neotagovaný, kde je viditeľný indikátor odhadnutého poradia; a jeden sken, kde sa varovanie o chýbajúcom texte naozaj vysloví. Každý z nich prejde cestou, ktorú šťastný prípad preskočí
Odtiaľ overte, že kurzor slova zostáva zamknutý pri dvojnásobnej aj polovičnej rýchlosti reči a že rolovanie cez ReadingWordFollow nezápasí s vlastným rolovaním používateľa. Potom nechajte reč bežať, kým prechádzate všetkými farebnými filtrami, a sledujte, či kurzor nikdy nezmizne. Túto vykresľovaciu cestu podrobne pokrýva článok o farebných filtroch pre slabozrakých a časovanie TTS rozoberá hĺbkový pohľad na rečový kurzor slova
Rozhrania pre jednotky čítania a slovné rámčeky použité vyššie sa dodávajú s komponentom PDFium Component pre Delphi a C++Builder (VCL) aj Lazarus/FPC (LCL). Produktová stránka odkazuje na úplnú referenciu rozhrania vrátane rozloženia záznamov pre jednotky čítania a slovné rámčeky, ktoré stoja za týmito príkladmi