Odborný článok

Vytvorte prístupnú čítačku PDF v Delphi s PDFium

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

Prístupná čítačka PDFium v Delphi berie poradie čítania z otagovaného stromu štruktúry a pri neotagovaných PDF sa vracia k heuristickej analýze rozloženia, pričom pole Source záznamu TPdfReadableContent odlíši rosStructure od rosHeuristic
Otagovaný strom ohlási nadpisy aj poradie riadkov, kým heuristická záloha háda z geometrie, a pole Source drží overené oddelené od odhadnutého

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

ReadingUnits v PDFium pre Delphi premení aktívnu stranu na text, sémantickú rolu a obdĺžniky v priestore strany, ktoré napĺňajú front reči čítačky obrazovky po jednej jednotke na slot, pričom prázdne pole jednotiek označí naskenovanú stranu na vyslovené varovanie
Jednotky čítania padajú po jednej do slotov frontu reči a prázdne pole na strane s obsahom je detektorom naskenovanej strany

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

Udalosti hranice slova zo SAPI v čítačke PDFium pre Delphi sa cez TrackReadingWordAt mapujú na geometriu PageWordBoxes a vykresľujú karaoke kurzor slova, pričom návratová hodnota -1 kurzor zmaže, keď offset prebehne za text strany
Offset hranice z TTS sa vyrieši na slovný rámček a vykreslí kurzor, a hodnota -1 za textom strany kurzor zmaže namiesto vyvolania výnimky

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