Technical Article

Izdelava dostopnega bralnika PDF v Delphi s PDFium

Slepi uporabnik odpre četrtletno poročilo v vašem novem pregledovalniku Delphi, vklopi bralnik zaslona NVDA in zasliši nogo strani, nato stolpec s številkami, šele nato pa naslov, ki bi ga vsak polnočuten bralec prebral najprej. Ali pa ne zasliši ničesar. Stran je na zaslonu videti popolna in prav to je past: upodabljanje in branje sta različna problema, ki ju rešuje različna koda. Vrstni red, v katerem PDF izrisuje svoje glife, ni nujno enak vrstnemu redu, v katerem bi jih morala oseba slišati. Zato pregledovalnik, zgrajen le na klicih za upodabljanje, ustvari brezhibno sliko, a neuporabno pripoved. Iz tega razloga PDFium Component, ovitek VCL/LCL okoli ogrodja PDFium za Delphi, C++Builder in Lazarus, prinaša ločen nabor bralnih vmesnikov API. Narisovalni vmesniki API ne morejo obnoviti vrstnega reda branja, ki jim nikoli ni bil podan.

Dostopen bralnik stoji ali pade na treh stvareh. Izluščiti mora vrstni red, ki ga bralnik zaslona lahko izgovori, ohranjati viden kazalec besed na tistem, kar glas trenutno bere, in odkriti, kdaj dokument sploh ni bil označen, namesto da ugiba in se pretvarja. Vsaka od teh nalog ima jasen API, ki ga lahko uporabite, ter pasti, ki vas lahko presenetijo, če izpustite podrobnosti.

Vrstni red branja se nahaja v strukturnem drevesu, ne v vrstnem redu izrisovanja

ISO 32000-1 §14.8 definira logično strukturo kot drevo elementov, naloženih nad vsebino strani. PDF/UA (ISO 14289-1) gre še korak dlje in to drevo določa kot obvezno: vsak del dejanske vsebine mora biti dosegljiv skozi to drevo v vrstnem redu branja, medtem ko se artefakti strani preskočijo. Pravilno označeno poročilo ve, da je naslov "Quarterly Results" (Četrtletni rezultati) naslov druge stopnje in da je tabela s skupnimi vrednostmi dejansko tabela z glavo. Neoznačeno poročilo pa je le kup nameščenih znakovnih zaporedij, ki so slučajno videti kot dokument.

Ko je struktura prisotna, jo funkcija ReadablePageContent preišče in vrne fragmente, označene s semantično lastnostjo Kind (vrednosti, kot sta cfHeading in cfParagraph). Tako lahko uporabniški vmesnik pred izgovorjavo besedila naznani »naslov«, namesto da bi odebeljeno vrstico prebral kot navadno besedilo. Če uporabnega drevesa ni, isti klic uporabi hevristično analizo postavitve: zazna stolpce, združi osnovne črte pisave ter razvrsti vsebino od leve proti desni in od zgoraj navzdol. Ta zasilna rešitev je primerna za enostavne enostolpčne dokumente, a nezanesljiva za glasila, večstolpčne obrazce, stranske vrstice ali poudarke. Ključno je vedeti, kateri rezultat ste prejeli, kar vam API pove neposredno. Zapis TPdfReadableContent vsebuje polje Source, ki je nastavljeno na rosStructure, ko je vrstni red določen iz označenega drevesa, ali rosHeuristic, ko je bil izpeljan iz geometrije. Prikazovanje ugibanega vrstnega reda, kot da je preverjen, je enako izdaji programske opreme z značko uspešnosti za gradnjo, ki je nihče ni zagnal.

Preprost korak ob odpiranju je, da preberete lastnost IsTagged in enkrat pokličete ValidatePdfUa, nato pa odgovor shranite v predpomnilnik. Neuspešno preverjanje PDF/UA check ni razlog za zavrnitev datoteke. Je pa razlog, da v statusno vrstico izpišete »ocenjen vrstni red branja«. Tako podporna ekipa ob pritožbi stranke glede popačene izgovorjave že vnaprej ve, ali gre za težavo z označevanjem v datoteki ali za hrošča v vaši kodi.

Od strani do čakalne vrste govorov z ReadingUnits

Za pretvorbo besedila v govor (TTS) opravi večino dela lastnost ReadingUnits. Vrne polje zapisov TPdfReadingUnit za aktivno stran, pri čemu vsak zapis vsebuje besedilo za izgovorjavo, njegovo semantično vlogo in pravokotnike, ki določajo njegov položaj na strani. Za neprekinjeno branje skozi strani obstaja tudi lastnost na ravni celotnega dokumenta: DocumentReadingUnits. Ena enota se neposredno uvrsti v eno mesto čakalne vrste govorov:

procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
  Units: TPdfReadingUnits;
  i: Integer;
begin
  Pdf.PageNumber := PageNumber;   // ReadingUnits works on the active page
  Units := Pdf.ReadingUnits;
  FSpeechQueue.Clear;
  for i := Low(Units) to High(Units) do
    FSpeechQueue.Add(Units[i]);  // text + semantics + highlight rects
  FCurrentPage := PageNumber;
  SpeakNextUnit;
end;

Dve stvari v tej zanki je zlahka narediti narobe. Čakalno vrsto gradite za vsako stran posebej in jo ponovno zgradite vsakič, ko uporabnik navigira, saj bralne enote vsebujejo pravokotnike v koordinatah strani; čakalna vrsta, ki je ostala od tretje strani, bo svoje označbe izrisala na četrto stran. Poleg tega prazno polje Units na strani, ki očitno vsebuje vsebino, obravnavajte kot zaznavo strani, ki vsebuje le sliko. Skenirana stran je sestavljena iz slikovnih pik brez besedilne plasti pod njimi. Pravilen odgovor v tem primeru je izgovorjava opozorila (»ta stran ne vsebuje besedila za črpanje«) namesto tišine, ki je poslušalec ne more razlikovati od sesutja programa.

Besedilni kazalec, ki sledi glasu

Označevanje celotnega odstavka naenkrat deluje počasno za slabovidne uporabnike, ki med glasnim branjem z očmi sledijo besedilu. Označevanje na ravni posameznih besed (učinek karaoke) potrebuje dva dela: geometrijo vsake besede in način za preslikavo poročil o napredku mehanizma TTS v to geometrijo. Funkcija PageWordBoxes ponuja geometrijo v obliki zapisov TPdfWordBox, kjer vsak vsebuje besedilo besede, njen znakovni odmik, število znakov in pravokotnik na strani. Funkcija TrackReadingWordAt pa zagotavlja preslikavo. Posredujte ji položaj znaka, ki ga dogodek meje besede v vmesniku SAPI že sporoča, funkcija pa bo ta odmik pretvorila v indeks v polju besedilnih okvirov in z enim samim klicem označila ustrezno besedo.

procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
  // The view's word boxes come from the page the view displays.
  // Setting Pdf.PageNumber alone would not move the view
  PdfView.PageNumber := PageNumber;
  FWordBoxes := PdfView.PageWordBoxes;
end;

procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
  WordIdx: Integer;
begin
  // TrackReadingWordAt maps the offset AND paints the word cursor
  WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
  if WordIdx < 0 then
    PdfView.ClearReadingWord;  // boundary ran past the page text
end;

Pogoji uporabe so v enem pogledu zelo prizanesljivi, v drugem pa neusmiljeni. Prizanesljivi del: TrackReadingWordAt vzdržuje lasten predpomnilnik besedilnih okvirov za stran, ki ji sledi, zato ni potrebe po predhodnem nalaganju, prav tako pa se ne izvaja nobeno upodabljanje, saj besedilni okviri prihajajo neposredno iz besedilne plasti. Tudi storitev govora brez uporabniškega vmesnika (headless) lahko še vedno sledi položajem. Neusmiljeni del: znakovni indeks se mora nanašati na besedilo, ki ga je izluščila komponenta, in ne na kakšen očiščen niz, ki ste ga zgradili sami. Ko CharIndex preseže konec besedila strani, funkcija vrne -1, namesto da bi sprožila izjemo, kar se pogosto zgodi, ko mehanizem TTS sproži zadnji dogodek meje za končna ločila. Vrednost -1 vedno obravnavajte kot ukaz za »odstranitev kazalca« in nikoli kot napako.

Na strani prikaza lastnost ReadingWordColor določa barvo kazalca. Privzeta jantarna barva se dobro obnese na večini ozadij strani, vendar jo preizkusite pod vsakim filtrom prikaza, ki ga ponuja vaš pregledovalnik. Jantarni kazalec lahko popolnoma izgine pri inverziji barv, inverzija skupaj z govorom pa je natanko tisti način, na katerega delajo slabovidni uporabniki. Zato je ta kombinacija najpomembnejša za pravilno izvedbo, čeprav je hitri preizkusi nikoli ne zajamejo. Nastavite lastnost ReadingWordFollow na True in pogled bo samodejno pomaknil izgovorjeno besedo v vidno polje, kar je nujno pri povečanih straneh, ki segajo čez zaslon. Upoštevajte pravilo obsega: metoda SetReadingWord riše le na aktivni strani objekta TPdfView. Vnaprej se odločite, ali ročno pomikanje začasno zaustavi govor ali pa ima vedenje sledenja prednost, saj sicer glas bere naprej, medtem ko kazalec ostane nekje zunaj zaslona.

Dokumenti, ki lahko onemogočijo vaš bralnik

Nekaj vrst vhodnih dokumentov zanesljivo onemogoči naivno implementacijo, zato spadajo med trajne vzorce v testni zbirki in ne med enkratne hrošče, ki jih odpravite in pozabite.

  • Neoznačene datoteke z veliko besedila. Hevristični vrstni red je običajno pravilen za linearna poročila, a napačen takoj, ko se pojavi stranska vrstica ali poudarek. Označite vrstni red kot ocenjen, tako v uporabniškem vmesniku kot v diagnostičnem dnevniku, da bo težava kasneje prepoznavna.
  • Skenirane strani s slikami. Brez kakršne koli besedilne plasti. Zaznajte jih prek praznih bralnih enot in uporabnika usmerite na korak OCR pred uvozom, namesto da bralnik bere prazno stran.
  • Kombinacijski znaki in mešana pisava. Kombinacijske oznake Unicode se ne združijo vedno ena proti ena v vidne besede, zato se lahko število besedilnih okvirov razlikuje od tistega, kar pričakuje vaš lastni razčlenjevalnik. Polja besedilnih okvirov ne indeksirajte z odmiki, ki ste jih izračunali sami z deljenjem besedila; uporabljajte le indekse, ki jih vrne funkcija TrackReadingWordAt.

Preizkusite kot revizor, ne kot predstavitev

»Dokument je prebralo na glas« ne dokazuje ničesar. Uspešen preizkus, ki ga lahko zagovarjate, vključuje zagon končne različice s tremi datotekami ob vklopljenem bralniku NVDA: ena preverjeno označena datoteka, kjer so naslovi najavljeni kot naslovi, tabela pa je prebrana po vrsticah; ena preverjeno neoznačena datoteka, kjer je viden indikator ocenjenega vrstnega reda; ter skenirana datoteka, kjer se dejansko izgovori opozorilo o odsotnosti besedila. Vsaka od njih preveri pot, ki jo običajni primeri preskočijo.

Nato potrdite, da kazalec besed ostane poravnan tako pri dvojni kot pri polovični hitrosti govora in da se samodejno pomikanje ReadingWordFollow ne bori z uporabnikovim lastnim pomikanjem strani. Nato zaženite govor in preklopite skozi vse barvne filtre ter preverite, da kazalec nikoli ne izgine. Članek o barvnih filtrih za slabovidne podrobno opisuje to pot upodabljanja, članek o kazalcu besed za govor pa razčlenjuje časovne uskladitve TTS.

Vmesnika API za bralne enote in besedilne okvire, uporabljena zgoraj, sta del komponente PDFium Component za Delphi in C++Builder (VCL) ter Lazarus/FPC (LCL). Stran izdelka vsebuje povezave do celotne reference API, vključno s strukturami zapisov za bralne enote in besedilne okvire iz teh primerov.