Sesli okuma düğmesi (read-aloud button) bir öğleden sonra demo yapar ve ardından bir haftayı tüketir (eats a week). Öğleden sonraki sürüm sayfa metnini çıkarır, SAPI'ye verir ve ses alır (gets audio). Özelliği kullanılabilir (usable) kılan şey ise şu haftaya yayılan kısımdır: ses pencereyi (window) dondurmamalıdır (must not freeze), konuşulan kelime sesle senkronize bir şekilde sayfada aydınlanmalıdır (light up) ve Boşluk (Space) tuşu her şeyi duraklatmalıdır (pause). Bu makale, hızlı sürümün (quick version) atladığı üç parça için çalışan kod ile, bu işlem hattını (pipeline) doğrudan (raw) PDFium metin API'sine ve Windows Konuşma (Speech) API'sine karşı Delphi'de oluşturmaktadır: Söyleyiş (utterance) başına değil de bir kez yapılan COM yaşam döngüsü, gerçek kelime sınırı (word-boundary) olayları ve bir PDF alanı (PDF-space) kelime kutusunu boyayabileceğiniz (paint) bir dikdörtgene dönüştüren koordinat matematiği (coordinate math)
Düzenleyici (Regulatory) bağlam bir cümleye sığar: senkronize sesli okuma, WCAG 2.1'in belge yazılımından (document software) istediklerinin görüntüleyici (viewer) tarafındaki yarısıdır (half) ve ISO 14289-1 (PDF/UA), ona karşı (against) en iyi şekilde çalışan etiketli dosya (tagged-file) yarısını (half) tanımlar. PDFium Bileşeni (Component) üzerine geliştiriyorsanız, bu işlem hattına (pipeline) hiç ihtiyacınız olmayabilir: görüntüleyici (viewer), bir karakter uzaklığını (offset) tek bir çağrıda boyalı bir kelime vurgusu (highlight) ile eşleştiren (maps) yerleşik bir izleme imleci (tracking cursor) ile birlikte gelir ve bu, kelime kelime TTS vurgulama makalesinde (word-by-word TTS highlighting article) ele alınmıştır. Aşağıdakiler, görüntüleyici (viewer) uygulamasının tamamına sahip olduğunuzda ve işlem hattının (pipeline) kendisini istediğinizde kullanabileceğiniz (for when you want) şeylerdir
Bir iş parçacığı (thread) işler (renders), bir iş parçacığı konuşur (speaks)
Mimari (architecture) iki iş parçacığı (thread) ve bir sözleşmedir (contract). Kullanıcı arayüzü iş parçacığı (UI thread) sayfa bitmap'ini (bitmap) oluşturur (renders), yakınlaştırma (zoom) ve kaydırma (scroll) durumuna (state) sahiptir ve vurgu kaplamasını (highlight overlay) boyar (paints). Özel bir konuşma iş parçacığı (speech thread) SAPI sesine (voice) sahiptir ve ona başka hiçbir şey dokunmaz. Sözleşme incedir (thin): konuşma iş parçacığı (speech thread) ilerlemeyi (progress) karakter uzaklıkları (character offsets) olarak bildirir (reports) ve kullanıcı arayüzü iş parçacığı (UI thread) uzaklıkları dikdörtgenlere dönüştürür
Çoğu SAPI örneği, her bir söyleyişi (utterance) CoInitialize ve CoUninitialize ile sarar (wraps) ve bir görüntüleyici (viewer) bunun neden yanlış (wrong) olduğunu hemen gösterir. SVSFlagsAsync ile yapılan Speak işlemi, metin kuyruğa alınır alınmaz (queued) geri döner (returns), bu nedenle aynı prosedürün finally bloğundaki bir CoUninitialize işlemi ses (voice) halen konuşurken çalışır (runs) ve ona sahip olan (owns) COM dairesini (apartment) yıkar (tearing down). Zamanlamaya (timing) bağlı olarak, sessizlik (silence), kesilmiş bir söyleyiş (truncated utterance) veya dakikalar sonra bir erişim ihlali (access violation) alırsınız. Doğru ömür (lifetime) sıkıcıdır (boring): konuşma iş parçacığı (speech thread) başladığında bir kez CoInitialize işlemi yapılır, ses (voice) bu daire (apartment) içinde yaratılır ve iş parçacığı (thread) çıkarken, ses serbest bırakıldıktan (freed) sonra bir kez CoUninitialize işlemi yapılır. Asla söyleyiş başına (per utterance) değil
Sese (voice) ayrıca bir mesaj pompası (message pump) gerekir, bu da nerede yaşayabileceğine (live) karar verir. SpVoice otomasyon nesnesi (automation object), olaylarını (events), onu oluşturan (created) iş parçacığının mesaj kuyruğu (message queue) aracılığıyla iletir. Onu kullanıcı arayüzü iş parçacığı (UI thread) üzerinde yaratın ve olaylar gerçekten ulaşır (arrive), çünkü VCL (Visual Component Library) mesajları pompalar, ancak ardından her yavaş (slow) boyama (paint) işlemi kelime sınırlarınızı (word boundaries) geciktirir (delays); onu pompası olmayan bir çalışan (worker) iş parçacığı üzerinde yaratın ve olaylar (events) hiçbir zaman ulaşmaz. Kendi GetMessage döngüsüne sahip özel bir iş parçacığı (dedicated thread), kullanıcı arayüzü (UI) ne yaparsa yapsın sınır gecikmesini (boundary latency) düz (flat) tutar
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; // FText'i FLock altında okur
procedure VoiceWord(ASender: TObject; StreamNumber: Integer;
StreamPosition: OleVariant; CharacterPosition, WordLength: Integer);
protected
procedure Execute; override;
procedure TerminatedSet; override;
public
procedure SpeakPage(const AText: string); // kullanıcı arayüzü iş parçacığından güvenli
end;
procedure TSpeechThread.Execute;
var
Msg: TMsg;
begin
CoInitialize(nil); // bir kez, iş parçacığı başladığında
try
FVoice := TSpVoice.Create(nil);
try
FVoice.EventInterests := SVEWordBoundary or SVEEndInputStream;
FVoice.OnWord := VoiceWord;
// İleti kuyruğunu (message queue), herhangi biri ona mesaj göndermeden önce (posts to it) bu iş parçacığı (thread) için oluşturmaya (creation) zorla
PeekMessage(Msg, 0, WM_USER, WM_USER, PM_NOREMOVE);
while GetMessage(Msg, 0, 0, 0) do // WM_QUIT ulaştığında (arrives) çıkar (exits)
if Msg.message = WM_SPEAK_PAGE then
FVoice.Speak(NextUtterance, SVSFlagsAsync or SVSFPurgeBeforeSpeak)
else
DispatchMessage(Msg); // SAPI olayı (event) geri çağrılarını (callbacks) iletir (delivers)
finally
FVoice.Free;
end;
finally
CoUninitialize; // bir kez, iş parçacığı (thread) çıkarken (exits)
end;
end;
procedure TSpeechThread.TerminatedSet;
begin
inherited;
PostThreadMessage(ThreadID, WM_QUIT, 0, 0); // GetMessage engellemesini (block) kaldır
end;
TerminatedSet işlemi, WM_QUIT mesajı gönderir (posts), böylece görüntüleyici (viewer) kapandığında (shuts down) pompa (pump) engelini kaldırır (unblocks). Kullanıcı arayüzü iş parçacığından çağrılan SpeakPage, metni kilit korumalı (lock-guarded) bir alanda (field) saklar ve WM_SPEAK_PAGE gönderir (posts), çünkü FVoice üzerindeki bir yöntemi (method) doğrudan başka bir iş parçacığından çağırmak (calling), dönüştürülmemiş (unmarshaled) bir arayüz (interface) üzerinde daireler arası (cross-apartment) bir COM çağrısı (call) olurdu. Döngüden önceki tek satırlık PeekMessage işlemi, Windows'u iş parçacığının mesaj kuyruğunu oluşturmaya zorlayarak, kullanıcı arayüzü iş parçacığından gelecek erken bir gönderinin (post) başarısız olacağı (fail) başlangıç yarışını (startup race) kapatır (closing)
Kelime sınırları karakter uzaklıkları (offsets) olarak ulaşır
Microsoft Konuşma Nesne Kitaplığını (Microsoft Speech Object Library) IDE'nin tür kitaplığı içe aktarıcısı (type library importer) aracılığıyla bir kez içe aktardığınızda (import), TSpVoice sarmalayıcısı (wrapper) ve türlenmiş olayları (typed events) ile SpeechLib_TLB öğesini (item) elde edersiniz (get). İki ayar (setting) önemlidir. EventInterests gerçekten (actually) tükettiğiniz olaylara daraltılmalıdır (narrowed), çünkü açık (switched on) bırakılan her ilgi (interest), her sayfanın (page) her kelimesi (word) için iş parçacıkları arası (cross-thread) olay trafiğidir (traffic); SVEWordBoundary vurguyu yönlendirir ve SVEEndInputStream söyleyişin (utterance) bittiğini (finished) söyler. Ve OnWord işleyicisi (handler), Speak işlevine geçirdiğiniz dizeye doğrudan indeksleme (index) yapan bir CharacterPosition (karakter pozisyonu) ve uzunluk (length) alır —başka hiçbir şeye (anything else) değil, konuşma arabelleğine (speech buffer) olan bir uzaklık (offset)
Son yan tümce (clause), özelliğin dayandığı değişmezdir (invariant): uzaklıklar yalnızca sesin okuduğu dizeye karşı (against) anlamlıdır (meaningful), bu nedenle tam olarak (exactly) çıkardığınız metni karakter karakter (character for character) konuşturun (speak). Boşlukları (whitespace) kırpın (trim), satır sonlarını (line breaks) daraltın (collapse) veya daha hoş (nicer) bir telaffuz (pronunciation) için bir kısaltmayı genişletin (expand); ilkinden sonraki her vurgu (highlight) bir kelime sapar (off). Kullanıcı arayüzü (UI) konuşulan materyali —sayfa duyuruları, başlık (heading) önekleri (prefixes)— dahil etmeliyse (inject), her eklemenin (insertion) konumunu ve uzunluğunu (length) kaydedin (record) ve onu eşleştirmeden (mapping) önce biriken kaymayı (accumulated shift) her uzaklıktan çıkarın (subtract)
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
// Konuşma iş parçacığında çalışır; uzaklıkları engellemeden kullanıcı arayüzüne verin
TThread.Queue(nil,
procedure
begin
ViewerForm.HighlightWordAt(CharacterPosition, WordLength);
end);
end;
TThread.Queue buradaki doğru mareşaldir (marshal), Synchronize değildir: işleyici (handler), kullanıcı arayüzü (UI) yeniden boyanırken (repaints) konuşma iş parçacığını park etmemelidir (must not park) ve sınır (boundary) olayları (events) ekranın (screen) çizilmesinden (draws) daha hızlı (faster) ulaşırsa (arrive), eski (stale) bir vurgu (highlight) güncellemesi zararsızdır (harmless) çünkü bir sonraki (next) güncelleme onun üzerine yazar (overwrites). Vurguyu temizlemek (clear) için OnEndStream olayını aynı (same) şekilde (way) bağlayın (wire) ve sürekli (continuous) okuma (reading) modunda (mode), bir sonraki (next) sayfanın metnini yüklemek (load) ve bir sonraki söyleyişi (utterance) göndermek (post) için
Karakter uzaklıklarından (offsets) ekrandaki piksellere (pixels)
PDFium geometriyi (geometry) karakter başına bildirir. FPDFText_GetCharBox, dört (four) çifte (doubles) değeri (values) —Windows'un sol, üst, sağ, alt (left, top, right, bottom) sırasıyla değil, sol, sağ, alt, üst (left, right, bottom, top) sırasıyla— doldurur (fills), bu da metin (text) API'sinde (API) sessiz (silent) hataların (bugs) diğerlerinden (anything else) daha fazla (more) nedeni olmuştur (caused), ve bunları sayfa (page) alanında (space) raporlar (reports): PDF noktaları (points), inç (inch) başına 72, orjin (origin) sol alt köşede (bottom-left corner), Y ekseni yukarıya doğru büyür (growing upward). Bir kelimenin kutusu (word's box), karakterlerinin kutularının bileşimidir (union) ve cihaz (device) piksellerine (pixels) dönüşüm üç adımdır: sayfa (page) başlangıcına (origin) göre çevirin (translate), yakınlaştırma (zoom) çarpı ekran (screen) DPI (DPI) bölü (over) 72 ile ölçekleyin (scale) ve Y eksenini (axis) çevirin (flip)
uses
System.Math;
type
TPdfRectF = record
Left, Top, Right, Bottom: Double; // PDF noktaları, orjin sol-alt (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
// Parametre sırası (order) sol, sağ, alt, üsttür - Windows sırası değil
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
// inç (inch) başına 72 PDF noktası; FZoom görüntüleyici (viewer) ölçek katsayısıdır (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 ekseni alt kenardan (bottom edge) yukarı doğru (upward) büyür; cihaz (device) Y ekseni aşağı doğru (downward) büyür
Result.Top := Round((FPageTop - W.Top) * Scale) - FScrollY;
Result.Bottom := Round((FPageTop - W.Bottom) * Scale) - FScrollY;
end;
FPageTop, FPDF_GetPageHeight değerinden (from) alınan nokta (points) cinsinden sayfa (page) yüksekliğidir (height) ve FPageLeft, çoğu belge için (most documents) sıfırdır, ancak sayfa (page) bir tane tanımladığında (defines) kırpma (crop) kutusundan (box) gelir (comes), bu nedenle (so) varsaymak (assuming) yerine (rather than) her ikisini de (both) FPDF_GetPageBoundingBox işlevinden okuyun (read). Y ekseni dönüşü (flip), elle (hand-rolled) yapılan sürümlerin bozulduğu (break) yerdir: cihaz (device) dikdörtgeninin (rectangle) üstü (top), sayfa üstünden (page top) aşağıya (down) doğru ölçülen PDF kutusunun (box) üstünden (top) gelir (comes). Bunu (it) ters (backwards) yaparsanız (get), her vurgu (highlight) sayfanın yanlış (wrong) yarısına yansıtılmış (mirrored) olarak (as) boyanır (paints)
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); // eski kelimeyi (old word) sil (erase)
InvalidateRect(PageBox.Handle, @FHighlightRect, False); // yenisini (new one) çiz (draw)
end;
procedure TViewerForm.PageBoxPaint(Sender: TObject);
var
Blend: TBlendFunction;
begin
PageBox.Canvas.Draw(0, 0, FPageBitmap); // işlenen (rendered) sayfa ilk (first), her zaman
if FHighlightRect.IsEmpty then Exit;
Blend.BlendOp := AC_SRC_OVER;
Blend.BlendFlags := 0;
Blend.SourceConstantAlpha := 96; // yaklaşık (about) yüzde 38 (38 percent) opaklık (opacity)
Blend.AlphaFormat := 0; // sabit (constant) alfa, piksel başına veri yok
Winapi.Windows.AlphaBlend(PageBox.Canvas.Handle,
FHighlightRect.Left, FHighlightRect.Top,
FHighlightRect.Width, FHighlightRect.Height,
FHighlightBrush.Canvas.Handle, 0, 0, 1, 1, Blend);
end;
Boya (paint) işleyicisi (handler), önce sayfa bitmap'ini (bitmap), sonra (after) da vurguyu (highlight) çizer, her seferinde (every time), böylece (so) kaplamanın (overlay) kendini (itself) asla (never) silmesi (erase) gerekmez (has to); eski (old) ve yeni (new) dikdörtgenleri (rectangles) geçersiz (invalidating) kılmak, yeniden boyama (repaint) bölgesini (region) hızlı (fast) konuşma (speech) hızlarında (rates) bile küçük tutar. FHighlightBrush, (for) başlangıçta (startup) bir kez (once) vurgu rengiyle (highlight color) doldurulan (filled) bire bir (one-by-one) TBitmap'tir (Bitmap) —bir amber için (for) FHighlightBrush.Canvas.Pixels[0, 0] := $0032C8FF— ki AlphaBlend hedef (target) dikdörtgenin (rectangle) üzerine uzanır (stretches), bu nedenle kare (frame) başına (per) hiçbir şey tahsis edilmez (allocated), ve 96 değerindeki SourceConstantAlpha, kelimenin renk tonu (tint) boyunca (through) okunaklı (legible) kalmasını sağlar (keeps). Rengi ters (inverted) ve yüksek kontrastlı (high-contrast) ekran modlarında (display modes) test edin (test); az gören bir (a low-vision) kullanıcının göremediği (cannot see) bir kaplama, tam olarak (exactly) o (the) kişi için yapıldığı için (built for) mevcut değildir (does not exist)
Okuma düzeni, metin API'sinin çözmeyeceği kısımdır
FPDFText_GetText karakterleri bazı uzamsal temizliklerle (spatial cleanup) içerik akışından (content stream) türetilen (derived) bir düzende (order) geri (back) verir (hands), ve tek (single-column) sütunlu bir rapor için bu düzen (order) uygundur (fine). Başka bir yerde (anywhere else) doğru (right) olmak (be) gibi bir yükümlülüğü (obligation) yoktur (has). İki sütunlu (two-column) bir bülten (newsletter) her iki sütunu da doğrudan (straight across) okuyabilir (read), bir kenar çubuğu (sidebar) bir cümleyi cümlenin ortasında (mid-clause) kesebilir (interrupt) ve bir altbilgi (footer) sayfanın ortasına (middle) gelebilir (arrive). Bunu düzelten (fixes) bilgi (information) —etiketli (tagged) PDF'lerin taşıdığı ve PDF/UA'nın zorunlu (mandatory) kıldığı ISO 32000-1 §14.8'in mantıksal yapı ağacı (logical structure tree)— ham (raw) metin (text-page) sayfası çağrıları (calls) tarafından (by) hiç (at all) sorgulanmaz (consulted). Kökenine (origin) dair açık (explicit) bir sinyal içeren, yapısal (structure-aware) bir düzene (order) ihtiyacınız varsa, bu bir raf (shelf) yukarıdaki (up) çözülmüş (solved) bir sorundur: PDFium Bileşeninin (Component's) okuma (reading) API'si (API), rosStructure veya rosHeuristic Source alanına sahip bir içerik döndürür ve erişilebilir (accessible) PDF okuyucu makalesi (reader article) onu adım adım (walks through) açıklar (it). Ham (raw) API seviyesinde (level), savunulabilir (defensible) pozisyon (position), çıkarma (extraction) düzenini (order) bir tahmin (estimate) olarak ele almak (treat), kullanıcı arayüzünde (UI) öyle (so) olduğunu söylemek ve regresyon setinde (regression set) bir çok sütunlu (multi-column) belge (document) ve yalnızca resim (image-only) içeren bir tarama (scan) tutmaktır (keep), böylece (so) her iki hata (failure) modu (modes) da görünür (visible) kalır (stay)
Görüntüleyicinin (viewer) kendisinin (itself) klavye (keyboard) ile çalıştırılabilir (operable) olması gerekir (has to be)
Sesli çıktı (Speech output), görüntüleyiciyi (viewer) klavye (keyboard) erişiminden (access) mazur görmez (does not excuse); sesli (read-aloud) okumayı kullanma olasılığı (likely) en yüksek olan kişilerin bir fareye (mouse) uzanma olasılığı (likely) en düşüktür (least). Sayfa (page) paneline TabStop := True değeri ve görünür (visible) bir odak (focus) dikdörtgeni verin, ardından üç (three) tuşu (keys) işleyin: Boşluk (Space) tuşu FVoice.Pause ve FVoice.Resume arasında geçiş yapar (toggles) ve Sol (Left) ile Sağ (Right) tuşları, geri gitmek (go back) için negatif bir sayımla (negative count) FVoice.Skip('Sentence', 1) aracılığıyla (through) atlar (skip). SAPI'nin Skip işlevi yalnızca cümle ayrıntı düzeyini (sentence granularity) anlar, bu nedenle kelime düzeyinde (word-level) atlama (skipping), SVSFPurgeBeforeSpeak ile kayıttan yürütmeyi (playback) temizlemek (purging) ve en son izlediğiniz kelimenin uzaklığından (offset) yeniden (re-speaking) konuşmak anlamına gelir —ucuz (cheap), çünkü vurgu (highlight) kodu zaten (already) o (that) uzaklığı tam (exactly) olarak depoluyor (storing). Ekran (screen) okuyucuların onu duyurabilmesi (announce) için her taşıma denetimini (transport control) bir başlık (caption) içeren gerçek (real) bir TButton olarak saklayın (keep)
Bu, işlem hattının tamamıdır (whole pipeline), tümü raw PDFium text API'sine (metin API) karşıdır: uygulamanın (app) ömrü (life) boyunca (for) COM'a ve sese sahip (owns) olan bir konuşma (speech) iş parçacığı (thread), kullanıcı (UI) arayüzüne karakter mesafeleri (offsets) olarak yönlendirilen (marshaled) sınır (boundary) olayları (events) ve karakter başına sayfa (page-space) alanı kutularının ekranda (on screen) karıştırılmış (blended) tek bir dikdörtgene dönüştürülmesi. Geometri ve izleme işlemine kendiniz (yourself) sahip olmayı (own) tercih etmiyorsanız, PDFium Component, kelime başına kutular, izleme imleci, otomatik kaydırmayı izleme ve cümle düzeyi okuma (reading) birimlerini bileşen özellikleri (component properties) olarak (as) sunar (ships), ve onun (its) sesli (read-aloud) okuma (aloud) demosu bu makalenin bir avuç çağrıya (calls) indirgenmiş (reduced) işlem (pipeline) hattıdır