Tlačidlo na hlasové čítanie je do dema hotové za jedno popoludnie, ale potom vás bude stáť rovno celý týždeň. Zatiaľ čo popoludňajšia verzia len vyextrahuje text stránky, odovzdá ho SAPI a získa späť zvuk, ten týždeň padne na to, čo túto funkciu robí reálne použiteľnou: prehrávaný hlas nesmie zamrznúť pri oknách, vyslovené slovo sa musí rozsvietiť na stránke synchrónne za sprievodu so zvukom a kláves Medzerník musí to celé okamžite pozastaviť. Tento článok v postupoch pripraví (builds) rovnako toto konkrétne spojovacie potrubie v prostredí Delphi oproti surovému textovému API z PDFium a rovnako tiež oproti API rozhraniu pre Windows Speech (Windows Speech API), spolu s prítomným funkčným kódom zachytávajúcim 3 hlavné kľúčové kúsky, ktorým sa rýchla nedeľná verzia len vyhýba (skips): za preberanie o životnosti pre COM vo forme jedenkrát k (done once) oproti na rovine ku každému pre (instead of per utterance), následné aj reálne skutočné hraničné udalosti (word-boundary events), ako i matematiku pre súradnice ohľadom pretváraní za (coordinate math that turns a PDF-space word box into a rectangle you can paint)
Z regulačného hľadiska by sa kontext dal ľahko umiestniť do jedinej vety: synchronizované čítanie na hlas je priam tou polovičnou časťou zo strany z prehliadačoch (viewer-side half of what WCAG 2.1 asks of document software), a ISO 14289-1 (PDF/UA) zas v postojoch u uplatnení v definovaní predstavuje druhú časť v (tagged-file half it works best against). Ak si ale budujete svoju vlastnú nadstavbu prostredníctvom od komponentu PDFium (building on PDFium Component), potom snáď z úvah tento kanál (pipeline) nebudete potrebovať vôbec: prehliadač už z prvej verzie dodáva v produkte zabudovaný sledovací kurzor, ktorý dokáže mapovať posuny pre znak (character offset) k namaľovanému a vysvietenému slovu len jediným volaním (painted word highlight in one call). Toto sprostredkovanie pokrýva článok o zvýrazňovaní slovo za slovom pre TTS z daného u (word-by-word TTS highlighting article). K nasledovnému s uvažovaním rovnako predstavených postupov pre uplatnenia sa dostávame vtedy, keď celá obslužná aplikácia prehliadača (viewer application) patrí priamo vám a hľadáte zavedenia tohto istého z prepojených vedení (pipeline itself)
Jedno vlákno vykresľuje (renders), jedno vlákno rozpráva (speaks)
Architektúra predstavuje dve vlákna (threads) a jeden kontrakt. Vo uplatneniach sa na formách slúži pre UI vlákno tak, že vykresľuje bitmapu pre stránku a priraďuje jej stav na pre priblíženia a u obsluhy v (scroll state), a vykreslí (paints) prekrytie na vrchu pre zvýraznenia (highlight overlay). Rovnako k tak vyhradené oddelené vlákno na reč (dedicated speech thread) zabezpečí o uchovanie a riadenie vlastného hlasu SAPI (SAPI voice) z prístupoch bez toho aby sa k nemu niekto ďalší dotýkal. Kontrakt (contract) o kategórii za prepojovanej časti vo tenkej rovine slúži: vlákno na reč iba podáva priradený postup pri vývoji reprezentovaný do odsadení u z k pre znaky (character offsets), zatiaľ čo samotné UI vlákno pretvára tieto posuny rovnako u k formátom na ohraničené a zobrazené k obdĺžnikom (rectangles)
Vo väčšine z ponúkaných z uvažovaných príkladoch pre SAPI obalia každé priradenie vo výstupe z u (utterance) do obaloch s priradeniami v CoInitialize a CoUninitialize. Hneď na to pre prehliadač o uplatneniach slúži presne príkladmi pre ukázania z dôvodov na omyl (wrong immediately). Výstup u volaniach Speak z s ohraničení spolu so (with SVSFlagsAsync returns as soon as the text is queued), sa hneď rovnako a úspešne vracia k u z (so a CoUninitialize in the same procedure's finally block runs while the voice is still speaking). Za voľby k trhaniu z prepojení slúžiac (tearing down the COM apartment that owns it). O k u (Depending on timing you get silence, a truncated utterance, or an access violation minutes later). K zachovaných o sféry pre oblasť rovnako od uplatnení v postupoch o (The correct lifetime is boring: CoInitialize once when the speech thread starts, create the voice inside that apartment, and CoUninitialize once when the thread exits, after the voice has been freed). Z u z (Never per utterance)
Hlas zároveň rovnako potrebuje ohraničenia pre pumpu na správy (message pump), ktorá obmedzeniach i vyčlení priestor na to kde môže reálne žiť. Objekt za priradených pre (The SpVoice automation object delivers its events through the message queue of the thread that created it). Ak sa z priradenia vytvorí rovnako na sféry u k (Create it on the UI thread and events do arrive, because the VCL pumps messages, but every slow paint then delays your word boundaries; create it on a worker thread with no pump and the events never arrive at all). O z (A dedicated thread with its own GetMessage loop keeps boundary latency flat no matter what the UI is doing)
uses
System.Classes, System.SyncObjs, Winapi.Windows, Winapi.Messages,
Winapi.ActiveX, SpeechLib_TLB;
const
WM_SPEAK_PAGE = WM_APP + 1;
type
TSpeechThread = class(TThread)
private
FVoice: TSpVoice;
FLock: TCriticalSection;
FText: string;
function NextUtterance: string; // reads FText under FLock
procedure VoiceWord(ASender: TObject; StreamNumber: Integer;
StreamPosition: OleVariant; CharacterPosition, WordLength: Integer);
protected
procedure Execute; override;
procedure TerminatedSet; override;
public
procedure SpeakPage(const AText: string); // safe from the UI thread
end;
procedure TSpeechThread.Execute;
var
Msg: TMsg;
begin
CoInitialize(nil); // once, when the thread starts
try
FVoice := TSpVoice.Create(nil);
try
FVoice.EventInterests := SVEWordBoundary or SVEEndInputStream;
FVoice.OnWord := VoiceWord;
// Force creation of this thread's message queue before anyone posts to it
PeekMessage(Msg, 0, WM_USER, WM_USER, PM_NOREMOVE);
while GetMessage(Msg, 0, 0, 0) do // exits when WM_QUIT arrives
if Msg.message = WM_SPEAK_PAGE then
FVoice.Speak(NextUtterance, SVSFlagsAsync or SVSFPurgeBeforeSpeak)
else
DispatchMessage(Msg); // delivers the SAPI event callbacks
finally
FVoice.Free;
end;
finally
CoUninitialize; // once, when the thread exits
end;
end;
procedure TSpeechThread.TerminatedSet;
begin
inherited;
PostThreadMessage(ThreadID, WM_QUIT, 0, 0); // unblock GetMessage
end;
K u z prečítaní za TerminatedSet pre k u v sfére o uplatnení zaradí volanie WM_QUIT rovnako aby sa pre (pump) obmedzila ochrana s (unblocks) vo (when the viewer shuts down). SpeakPage, ktoré na o volá k z (called from the UI thread, stores the text in a lock-guarded field and posts WM_SPEAK_PAGE, because calling a method on FVoice directly from another thread would be a cross-apartment COM call on an unmarshaled interface). Jediný u (The one-line PeekMessage before the loop forces Windows to create the thread's message queue, closing the startup race where an early post from the UI thread would fail)
Hranice slov prichádzajú ako posuny (offsets) pre znaky
Z priradení s uplatnením k v u SpeechLib_TLB k TSpVoice v (Import the Microsoft Speech Object Library once through the IDE's type library importer and you get SpeechLib_TLB with the TSpVoice wrapper and its typed events). Záleží vo u z o pre na z uplatnení (Two settings matter). K u sféry uplatnenia k od z (EventInterests should be narrowed to the events you actually consume, because every interest left switched on is cross-thread event traffic for every word of every page; SVEWordBoundary drives the highlight and SVEEndInputStream tells you the utterance finished). O k požiadavkám (And the OnWord handler receives CharacterPosition and a length, which index into the exact string you passed to Speak — an offset into the speech buffer, not into anything else)
Posledná z u klauzúl pre úloh preukázaných z obmedzení za invariantu pre vlastností stavaných z úloh sa na obmedzene u spojených pri zachovanú rovnako drží o úloh k z (hangs on: offsets are only meaningful against the string the voice is reading, so speak exactly the text you extracted, character for character). Orezaním k u uplatnení (Trim whitespace, collapse line breaks, or expand an abbreviation for nicer pronunciation, and every highlight after the first edit lands one word off). Z z u (If the UI must inject spoken material — page announcements, heading prefixes — record each insertion's position and length, and subtract the accumulated shift from every offset before mapping it)
procedure TSpeechThread.SpeakPage(const AText: string);
begin
FLock.Enter;
try
FText := AText;
finally
FLock.Leave;
end;
PostThreadMessage(ThreadID, WM_SPEAK_PAGE, 0, 0);
end;
procedure TSpeechThread.VoiceWord(ASender: TObject; StreamNumber: Integer;
StreamPosition: OleVariant; CharacterPosition, WordLength: Integer);
begin
// Runs on the speech thread; hand the offsets to the UI without blocking
TThread.Queue(nil,
procedure
begin
ViewerForm.HighlightWordAt(CharacterPosition, WordLength);
end);
end;
Z (TThread.Queue is the right marshal here, not Synchronize: the handler must not park the speech thread while the UI repaints, and if boundary events arrive faster than the screen draws, a stale highlight update is harmless because the next one overwrites it). Prepojovacie vo a o OnEndStream rovnakým (Wire OnEndStream the same way to clear the highlight, and in a continuous-reading mode, to load the next page's text and post the next utterance)
Z odsadení (offsets) pre znaky až po pixely na obrazovke
PDFium hlási geometriu pre každý znak samostatne. FPDFText_GetCharBox napĺňa štyri hodnoty typu double v poradí, ktoré v textovom API spôsobilo viac tichých chýb než čokoľvek iné — vľavo, vpravo, dole, hore (nie to štandardné pre Windows: vľavo, hore, vpravo, dole) — a hlási ich v priestore stránky: body PDF (points), kde 72 pripadá na palec, so začiatkom v ľavom dolnom rohu, kde Y rastie smerom nahor. Ohraničujúci box slova je zjednotením boxov jeho jednotlivých znakov a transformácia na pixely zariadenia predstavuje tri kroky: posun o začiatok stránky (translate), zmenu mierky pomocou priblíženia násobenú DPI obrazovky a lomeno 72 (scale) a nakoniec prevrátenie osi Y (flip)
uses
System.Math;
type
TPdfRectF = record
Left, Top, Right, Bottom: Double; // PDF points, origin bottom-left
end;
function TViewerForm.WordBox(CharIndex, CharCount: Integer): TPdfRectF;
var
i, LastChar: Integer;
L, T, R, B: Double;
begin
Result.Left := MaxDouble; Result.Bottom := MaxDouble;
Result.Right := -MaxDouble; Result.Top := -MaxDouble;
LastChar := Min(CharIndex + CharCount, FPDFText_CountChars(FTextPage)) - 1;
for i := CharIndex to LastChar do
begin
// Parameter order is left, right, bottom, top - not the Windows order
FPDFText_GetCharBox(FTextPage, i, @L, @R, @B, @T);
Result.Left := Min(Result.Left, L);
Result.Right := Max(Result.Right, R);
Result.Bottom := Min(Result.Bottom, B);
Result.Top := Max(Result.Top, T);
end;
end;
function TViewerForm.PdfToDevice(const W: TPdfRectF): TRect;
var
Scale: Double;
begin
// 72 PDF points per inch; FZoom is the viewer scale factor
Scale := FZoom * FScreenDpi / 72.0;
Result.Left := Round((W.Left - FPageLeft) * Scale) - FScrollX;
Result.Right := Round((W.Right - FPageLeft) * Scale) - FScrollX;
// PDF Y grows upward from the bottom edge; device Y grows downward
Result.Top := Round((FPageTop - W.Top) * Scale) - FScrollY;
Result.Bottom := Round((FPageTop - W.Bottom) * Scale) - FScrollY;
end;
FPageTop je výška stránky v bodoch získaná z FPDF_GetPageHeight a FPageLeft je pri väčšine dokumentov rovná nule, ale pochádza z orezávacieho boxu (crop box) v prípade ak ho stránka definuje, preto radšej načítavajte obe hodnoty z FPDF_GetPageBoundingBox miesto jednoduchého predpokladu. Prevrátenie osi Y je miestom, kde ručne písané verzie často zlyhávajú: horná časť obdĺžnika zariadenia pochádza z hornej časti boxu PDF, ale meraná je smerom nadol od horného okraja stránky. Zmýľte si to opačne a každé jedno zvýraznenie sa namaľuje prevrátené zrkadlovo do nesprávnej polovice stránky
procedure TViewerForm.HighlightWordAt(CharIndex, CharCount: Integer);
var
Old: TRect;
begin
if CharCount <= 0 then Exit;
Old := FHighlightRect;
FHighlightRect := PdfToDevice(WordBox(CharIndex, CharCount));
InvalidateRect(PageBox.Handle, @Old, False); // erase the old word
InvalidateRect(PageBox.Handle, @FHighlightRect, False); // draw the new one
end;
procedure TViewerForm.PageBoxPaint(Sender: TObject);
var
Blend: TBlendFunction;
begin
PageBox.Canvas.Draw(0, 0, FPageBitmap); // rendered page first, always
if FHighlightRect.IsEmpty then Exit;
Blend.BlendOp := AC_SRC_OVER;
Blend.BlendFlags := 0;
Blend.SourceConstantAlpha := 96; // about 38 percent opacity
Blend.AlphaFormat := 0; // constant alpha, no per-pixel data
Winapi.Windows.AlphaBlend(PageBox.Canvas.Handle,
FHighlightRect.Left, FHighlightRect.Top,
FHighlightRect.Width, FHighlightRect.Height,
FHighlightBrush.Canvas.Handle, 0, 0, 1, 1, Blend);
end;
Kresliaci obslužný program (paint handler) zakaždým vykreslí najprv bitmapu stránky a až potom samotné zvýraznenie, takže sa toto prekrytie nemusí nikdy samo zmazávať; zneplatnenie starých a nových obdĺžnikov udržuje oblasť na prekreslenie malú aj pri rýchlych vlnách reči. FHighlightBrush je TBitmap s veľkosťou jedenkrát jeden pixel naplnená pri štarte farbou zvýraznenia — FHighlightBrush.Canvas.Pixels[0, 0] := $0032C8FF pre jantárovú — ktorú AlphaBlend natiahne cez celú plochu cieľového obdĺžnika, a tak sa ani nevyčleňuje žiadna pamäť pre každú snímku. Hodnota SourceConstantAlpha na 96 pritom zabezpečuje, aby bolo slovo pod naneseným odtieňom aj naďalej čitateľné. Vyskúšajte túto farbu aj v režimoch s invertovaným zobrazením a vysokým kontrastom; ak používateľ so slabším zrakom nedokáže vidieť toto zvýraznenie, pre človeka pre ktorého ste ho priamo vyrobili, akoby ani neexistovalo
Poradie na čítanie je tá časť, ktorú textové API nevyrieši
Funkcia FPDFText_GetText vracia späť znaky v poradí, ktoré je odvodené z prúdu dát s obsahom, na ktoré nadväzuje s určitým priestorovým vyčistením (spatial cleanup), a pre jednosĺpcový report je toto poradie aj plne v poriadku. Ale už nemá žiadnu povinnosť doručiť správne výsledky z poradia úplne inde. Dvojsĺpcový leták sa dá čítať tak, že budete čítať rovno naprieč oboma stĺpcami, bočný panel (sidebar) môže prerušiť nejakú vetu priamo uprostred klauzuly a obsah na spodku pre pätu (footer) kľudne priletí až sem doprostred celej vety na stránku. Informácie, ktoré toto usporadúvajú a riešia — logický strom o štruktúre podľa normy ISO 32000-1 §14.8, akým oštítkované dokumenty formátu PDF (tagged PDFs) disponujú, pričom samotný PDF/UA ho robí plne povinným — s tými neprichádzajú a neporadia si bežné strohé volania na načítanie textových plôch z celých stránok už vonkoncom vôbec. Ak pre vaše postupy požadujete od usporiadania takého poradia zameranie so zmyslom na uplatnenú štruktúru u ohraničeniach v (structure-aware order) s výslovným signálom pôvodu z miesta, to v skutočnosti predstavuje o jednu poličku vyššie vyriešenú a uchovanú oblasť: API z komponentu PDFium Component v oblasti na čítanie vracia obsah už s poľom Source nastavenom v podobách uplatnených formátu na hodnotu pre rosStructure alebo na rosHeuristic, ako je predstavené v návode z článku venovanému pre prístupný vyhľadávač z PDF (accessible PDF reader). Na úrovni v surovom API v rovnako zastávanej a obhájiteľnej pozícií (defensible position) pristupujte v rovine pre extrakciu poradia iba ako k odhadu (treat extraction order as an estimate), vyjadrite sa rovnako priamo v používateľskom prostredí (v UI) a zachovajte aspoň jeden viac-sĺpcový (multi-column) dokument a jeden čisto oskenovaný (image-only scan) pri nasadeniach k regresnej súprave (regression set), aby oba tieto chybové prípady zostali dostatočne viditeľné (failure modes stay visible)
Samotný prehliadač sa musí dať ovládať klávesnicou
Rečový hlasový výstup v žiadnom z predstavení a nijako neospravedlňuje prehliadač od prístupu cez klávesnicu (keyboard access); ľudia, ktorí zvyknú najskôr túto funkciu s hlasovým čítaním (read-aloud) využívať aj naďalej (people most likely to use read-aloud), by sa vôbec najmenej pravdepodobne naťahovali za myšou v (least likely to reach for a mouse). Zariaďte u panelu stránky pre (Give the page panel TabStop := True) rovnako s priradením u viditeľného ohraničujúceho poľa (visible focus rectangle), a prejdite k spracovávaniam na troch z kláves: kláves Medzerník na striedavé prepínanie pre FVoice.Pause a FVoice.Resume v uplatnení a rovnako kláves Doľava a Doprava na krokovanie (skip) obmedzene o cez FVoice.Skip('Sentence', 1) so záporným počtom pre posun dozadu (with a negative count to go back). Obsluha v (SAPI's Skip only understands sentence granularity, so word-level skipping means purging playback with SVSFPurgeBeforeSpeak and re-speaking from the offset of the word you last tracked — cheap, since the highlight code is already storing exactly that offset). Zachovajte pri každom ovládači transportu reálny stav z TButton spolu s titulkom na oznam (Keep every transport control a real TButton with a caption so screen readers announce it)
Toto celkovo predstavuje kompletný (pipeline) spojený s obsluhou postavenou proti surovo sprístupnenému API (raw PDFium text API): o vlákno pre reč, ktoré drží COM a hlas počas celej životnosti v danej aplikácii, udalosti z rozhrania z posunov hraníc vo forme prislúchajúcich do UI za (boundary events marshaled to the UI as character offsets) a pre obdĺžniky vo priestoroch stránok pre každý znak zlúčených na jeden posunutý pre blended (blended rectangle on screen). Ak v úlohách si rovnako netrúfate urobiť vlastnými prostriedkami z obsluhy od na sledovaní (If you would rather not own the geometry and tracking yourself, PDFium Component ships per-word boxes, the tracking cursor, auto-scroll follow, and sentence-level reading units as component properties, and its read-aloud demo is this article's pipeline reduced to a handful of calls)