Teknisk artikkel

Bygg en tilgjengelig PDF-leser i Delphi med PDFium

En blind bruker åpner en kvartalsrapport i den blanke nye Delphi-fremviseren din, slår på NVDA, og hører sidefoten, deretter en kolonne med tall, deretter tittelen som enhver seende leser ville ha lest først. Eller hører ingenting i det hele tatt. Siden ser perfekt ut på skjermen, og det er nettopp fellen: rendering og lesing er forskjellige problemer løst av forskjellig kode. Rekkefølgen en PDF maler glyfene sine i, har ingen forpliktelse til å matche rekkefølgen et menneske bør høre dem i, så en fremviser bygget bare på rendringskall produserer et feilfritt bilde og en ubrukelig opplesning. PDFium Component, VCL/LCL-innpakningen rundt PDFium-motoren for Delphi, C++Builder og Lazarus, bærer et eget sett med lese-API-er av nettopp denne grunnen. Tegne-API-ene kan ikke gjenopprette en leserekkefølge de aldri ble gitt

En tilgjengelig leser står eller faller på tre ting. Den må hente ut en rekkefølge en skjermleser kan uttale, holde en synlig ordmarkør festet til hva stemmen enn sier, og innrømme når et dokument aldri ble tagget i stedet for å gjette og late som. Hver av dem har et klart API å gripe til og en feil som biter hvis du hopper over detaljen

Leserekkefølgen bor i strukturtreet, ikke i malerekkefølgen

ISO 32000-1 §14.8 definerer logisk struktur som et tre av elementer lagt over sideinnholdet. PDF/UA (ISO 14289-1) går lenger og gjør det treet obligatorisk: hver bit ekte innhold må være nåbart gjennom det i leserekkefølge, med sideartefakter merket som sådan og hoppet over. En korrekt tagget rapport vet at "Quarterly Results" er en overskrift på nivå to og at summeringsrutenettet er en tabell med overskriftsceller. En utagget rapport er en haug med posisjonerte glyfsekvenser som tilfeldigvis ser ut som et dokument

ReadablePageContent går gjennom det treet når det finnes, og gir tilbake fragmenter tagget med en semantisk Kind, verdier som cfHeading og cfParagraph, slik at UI-et kan si "overskrift" før ordene fremfor å lese en fet linje som vanlig brødtekst. Uten et brukbart tre faller det samme kallet tilbake på heuristisk layoutanalyse: oppdag kolonner, klynge grunnlinjer, sorter fra venstre til høyre og topp til bunn. Det fallbacket fungerer greit for et enkeltkolonne-notat og er skjørt for et nyhetsbrev, et flerkolonneskjema, alt med en sidestolpe eller et uthevet sitat. Det som betyr noe, er å vite hvilket resultat du fikk, og API-et forteller deg det rett ut. TPdfReadableContent-posten bærer et Source-felt satt til rosStructure når rekkefølgen kom fra det taggede treet, eller rosHeuristic når den ble utledet fra geometri. Vis en gjettet rekkefølge som om den var verifisert, og du har levert tilgjengelighetsversjonen av et bestått-merke på en build ingen kjørte

En PDFium tilgjengelig leser i Delphi tar leserrekkefølgen fra det taggede strukturtrærne og faller tilbake til heuristisk layoutanalyse for utaggede PDF-er, med TPdfReadableContent Source-feltet som skiller rosStructure fra rosHeuristic
Det taggede treet kunngjør overskrifter og radrekkefølge mens den heuristiske fallbacken gjetter fra geometri, og Source-feltet holder verifisert adskilt fra estimert

Det billige grepet ved åpningstidspunktet er å lese IsTagged og kalle ValidatePdfUa én gang, og så bufre svaret. En mislykket PDF/UA-sjekk er ingen grunn til å avvise filen. Det er grunn til å sette "estimert leserekkefølge" i statuslinjen, slik at når en kunde sender inn en klage om forvrengt opplesning, vet support allerede om de ser på et taggingproblem i filen eller en feil i koden din

Fra side til talekø med ReadingUnits

For tekst-til-tale gjør ReadingUnits det tunge løftet. Den returnerer et array med TPdfReadingUnit-poster for den aktive siden, hver holdende teksten som skal uttales, sin semantiske rolle, og rektanglene som lokaliserer den på siden. Det finnes en dokumentomfattende følgesvenn, DocumentReadingUnits, når du vil ha sammenhengende lesing på tvers av sider. Én enhet passer rett inn i én plass i en talekø:

procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
  Units: TPdfReadingUnits;
  i: Integer;
begin
  Pdf.PageNumber := PageNumber;   // ReadingUnits virker på den aktive siden
  Units := Pdf.ReadingUnits;
  FSpeechQueue.Clear;
  for i := Low(Units) to High(Units) do
    FSpeechQueue.Add(Units[i]);  // tekst + semantikk + uthevingsrektangler
  FCurrentPage := PageNumber;
  SpeakNextUnit;
end;

To ting i den løkken er lett å gjøre feil. Hold køen per side og bygg den på nytt hver gang brukeren navigerer, fordi leseenheter bærer rektangler i sidekoordinater; en kø som ble liggende igjen fra side tre, vil male uthevingene sine på side fire. Og behandle et tomt Units-array på en side som tydelig har innhold, som din bilde-bare-detektor. En skannet side er piksler uten noe tekstlag under, og det riktige svaret er å uttale en advarsel ("denne siden har ingen ekstraherbar tekst") fremfor å bli stille på en måte lytteren ikke kan skille fra en fastlåsing

PDFium ReadingUnits i Delphi gjør den aktive siden om til tekst, semantisk rolle og sideflate-rektangler som fyller en skjermleser talekø én enhet per plass, med en tom units-rekke som flagger en skannet side for en muntlig advarsel
Leseenheter slipper én per plass inn i talekøen, og en tom array på en innholdsbærende side er den skannede-side-detektoren

En ordmarkør som følger stemmen

Å utheve en hel paragraf om gangen føles trått for en svaksynt bruker som følger ordene med øynene mens de leses høyt. Ordnivå-uthevning, karaoke-effekten, trenger to deler: geometrien til hvert ord, og en måte å kartlegge TTS-motorens fremdriftsrapporter mot den geometrien. PageWordBoxes gir deg geometrien som TPdfWordBox-poster, hver med ordteksten, tegnforskyvningen, tegnantallet og et rektangel i sidekoordinater. TrackReadingWordAt gir deg kartleggingen. Mat den med tegnposisjonen SAPIs ordgrense-hendelse allerede rapporterer, og den løser den forskyvningen til en indeks i ordboks-arrayet og maler markøren på det matchende ordet i ett kall

procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
  // Visningens ordbokser kommer fra siden visningen viser.
  // Å sette Pdf.PageNumber alene ville ikke flytte visningen
  PdfView.PageNumber := PageNumber;
  FWordBoxes := PdfView.PageWordBoxes;
end;

procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
  WordIdx: Integer;
begin
  // TrackReadingWordAt kartlegger forskyvningen OG maler ordmarkøren
  WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
  if WordIdx < 0 then
    PdfView.ClearReadingWord;  // grensen løp forbi sideteksten
end;

Kontrakten er raus på ett punkt og ubarmhjertig på et annet. Den rause delen: TrackReadingWordAt holder sin egen ordboks-buffer for siden den sporer, så det er ingenting å forhåndslaste, og ingen rendering skjer i det hele tatt fordi ordboksene kommer fra tekstlaget. En hodeløs taletjeneste uten noe synlig vindu kan fortsatt spore posisjoner. Den ubarmhjertige delen: tegnindeksen må peke inn i teksten komponenten hentet ut, ikke inn i en eller annen opprenset streng du bygde selv. Når CharIndex løper forbi slutten av sideteksten, returnerer funksjonen -1 fremfor å kaste et unntak, noe som skjer hele tiden når en TTS-motor avfyrer en siste grensehendelse for etterfølgende tegnsetting. Les -1 som "fjern markøren", aldri som en feil

På visningssiden setter ReadingWordColor markørfargen. Standard rav-farge holder seg over de fleste sidebakgrunner, men test den under hvert visningsfilter fremviseren din tilbyr. En rav-farget markør kan forsvinne fullstendig under fargeinversjon, og inversjon som kjører sammen med tale er nøyaktig hvordan en svaksynt bruker arbeider, så den ene kombinasjonen du mest trenger å få riktig, er den en rask demo aldri utøver. Sett ReadingWordFollow til True, og visningen ruller det uttalte ordet inn i synsfeltet på egen hånd, noe du ikke kan klare deg uten på en zoomet side som sprer seg over flere skjermer. Husk én omfangsregel: SetReadingWord maler bare på den aktive TPdfView-siden. Bestem på forhånd om manuell rulling pauser talen eller om følgeatferden overstyrer den, for å velge ingen av delene lar stemmen fortsette å lese mens markøren sitter et sted utenfor skjermen

SAPI ordgrense-hendelser i en Delphi PDFium-leser mappes gjennom TrackReadingWordAt onto PageWordBoxes-geometrien for å male karaoke-ordmarkøren, med en -1-retur som nullstiller markøren når forskyvningen løper forbi sideteksten
TTS-grenseoffseten løses til en ord-boks og maler markøren, og en -1 forbi sideteksten tømmer den i stedet for å utløse

Dokumentene som bryter leseren din

En håndfull inndataformer overvinner en naiv implementasjon pålitelig nok til at de hører hjemme som permanente eksempler i regresjonssuiten, ikke som enkeltstående feil du fikser og glemmer

  • Utaggede, men tekstrike filer. Heuristisk rekkefølge har en tendens til å være riktig for en lineær rapport og feil i det øyeblikket en sidestolpe eller et uthevet sitat kommer inn. Flagg rekkefølgen som estimert, både i UI-et og i diagnostikkloggen din, slik at feilen er lesbar senere
  • Kun-bilde-skanninger. Intet tekstlag i det hele tatt. Fang dem opp gjennom tomme leseenheter og pek brukeren mot et OCR-trinn oppstrøms i stedet for å la leseren fortelle om en tom side
  • Kombinerende tegn og blandede skriftsystemer. Unicode-kombineringsmerker faller ikke alltid sammen én-til-én til visuelle ord, så ordboks-antallet kan drifte bort fra det din egen tokenizer forventer. Ikke indekser ordboks-arrayet med forskyvninger du selv beregnet ved å splitte tekst; bruk bare indeksene TrackReadingWordAt returnerer

Test den som en revisor, ikke som en demo

"Den leste eksempelet mitt høyt" beviser ingenting. En godkjenning du kan forsvare, kjører tre filer gjennom den ferdige builden med NVDA tilkoblet: én kjent-tagget fil, hvor overskrifter kunngjøres som overskrifter og en tabell leses i radrekkefølge; én kjent-utagget fil, hvor indikatoren for estimert rekkefølge er synlig; og en skanning, hvor advarselen om ingen tekst faktisk uttales. Hver av dem utøver en sti lykke-tilfellet hopper over

Derfra, bekreft at ordmarkøren holder seg låst på ved dobbel taletempo og ved halv, og at ReadingWordFollow-rullingen ikke kjemper mot brukerens egen rulling. Kjør så tale mens du sykler gjennom hvert fargefilter og se etter at markøren aldri forsvinner. Artikkelen om fargefiltre for svaksynte dekker den rendringsstien i detalj, og dypdykket i ordtale-markøren plukker fra hverandre TTS-timingen

Lese-enhets- og ordboks-API-ene brukt over, følger med PDFium Component for Delphi og C++Builder (VCL) og Lazarus/FPC (LCL). Produktsiden lenker til den fullstendige API-referansen, inkludert postoppsettene for leseenheter og ordbokser bak disse eksemplene