Die erste Demo unserer Vorlesefunktion für eine Lern-App lief zwei Absätze lang sauber. Dann erreichte die Seite eine Initiale, die Stimme sagte „Kapitel“, während die Hervorhebung noch in der vorherigen Zeile stand, und am Seitenende hinkte der Cursor dem Ton um drei Wörter hinterher. Die Stimme war nie das Problem — SAPI meldete Wortgrenzen präzise. Das Problem war die Abbildungsschicht zwischen Zeichenpositionen im Sprachpuffer und Rechtecken auf einer gerenderten PDF-Seite, und genau in dieser Schicht entscheidet sich, ob jede karaokeartige Hervorhebung funktioniert oder driftet. PDFium Component liefert diese Abbildung für Delphi, C++Builder und Lazarus seit v1.53 mit Wortboxen und seit v1.56 mit Tracker und Lesecursor als kleine, bewusst geschnittene API: Wortboxen, einen Offset-zu-Wort-Tracker und einen Hervorhebungscursor mit Auto-Scroll. In der richtigen Reihenfolge ist das robust; in der falschen Reihenfolge erzeugt es genau den Drift, den wir vorgeführt haben.
Zeichen sind keine Wörter, und TTS-Engines sprechen in Zeichen
Eine Sprach-Engine verarbeitet eine flache Zeichenkette und meldet den Fortschritt als Zeichenpositionen innerhalb dieser Zeichenkette. Eine PDF-Seite dagegen besitzt Glyphen in Seitenkoordinaten, wobei ein „Wort“ ein heuristischer Cluster aus Glyphenläufen ist. Diese beiden Koordinatensysteme haben nichts gemeinsam, wenn der Text, den Sie dem Synthesizer geben, nicht Byte für Byte dem Text entspricht, aus dem die Wortboxen berechnet wurden. Das ist Regel eins, und sie verzeiht nichts: Normalisieren Sie Leerraum, entfernen Sie weiche Trennzeichen oder „säubern“ Sie den extrahierten Text vor dem Vorlesen, dann ist jeder nachfolgende Offset stillschweigend falsch. Sprechen Sie exakt das, was Sie extrahiert haben, oder pflegen Sie eine explizite Offset-Remapping-Tabelle — eine dritte Option, die echte Dokumente überlebt, gibt es nicht.
Die Remapping-Variante ist nicht theoretisch. Wenn Ihre Oberfläche gesprochene Seitenansagen einfügt („Seite fünf“) oder Abkürzungen für den Synthesizer ausschreibt, protokollieren Sie Position und Länge jeder Einfügung und ziehen Sie die aufsummierte Korrektur vor jedem Tracking-Aufruf ab. Das sind zwanzig Zeilen Buchführung und der Unterschied zwischen einer Hervorhebung, die mit Funktionszuwachs mitwächst, und einer, die beim ersten Wunsch nach gesprochenen Überschriften bricht.
Was eine Wortbox liefert
Jeder TPdfWordBox-Datensatz trägt den Text des Wortes, seinen StartIndex und die Zeichenanzahl Count im Seitentext, ein seitenbezogenes Rect und die 1-basierte Seitennummer Page. PageWordBoxes liefert das vollständige Array für die aktive Seite:
procedure TReaderForm.PreparePage(PageNo: Integer);
begin
PdfView.PageNumber := PageNo; // the view's word boxes track its displayed page
FWords := PdfView.PageWordBoxes;
FPageText := BuildSpeechText(FWords); // concatenate Word.Text in order
if Length(FWords) = 0 then
HandleImageOnlyPage(PageNo); // a scan with no text layer
end;
Der Kommentar zur Reihenfolge ist tragend: Das PageWordBoxes des Viewers tokenisiert die Textebene der Seite, die der Viewer gerade anzeigt. Navigieren Sie also zuerst den Viewer und extrahieren Sie danach — Rendering ist nicht erforderlich, nur ein geöffnetes Dokument. Die Dokumentkomponente bietet für Headless-Nutzung ein eigenes PageWordBoxes, das an Pdf.PageNumber gebunden ist. Ein leeres Ergebnis auf einer Seite mit sichtbarem Inhalt bedeutet einen Nur-Bild-Scan. Leiten Sie ihn an OCR weiter oder überspringen Sie ihn hörbar („Seite 4 enthält keinen lesbaren Text“), statt die Stimme ohne Erklärung verstummen zu lassen.
SAPI-Wortgrenzen mit dem Tracker verbinden
TrackReadingWordAt am Viewer ist das Scharnier der gesamten Funktion: Übergeben Sie eine Seitennummer und einen Zeichenindex, dann findet der Aufruf die Wortbox, die dieses Zeichen enthält, zeichnet den Lesecursor darauf und gibt den Wortindex zurück — oder −1. Die Wortgrenzenbenachrichtigung von SAPI liefert genau die Zeichenposition, die Sie brauchen:
procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
WordIdx: Integer;
begin
// Maps the offset to a word box and moves the highlight in one call
WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
if WordIdx < 0 then
Exit; // boundary fell outside any word: keep last highlight
end;
Zwei defensive Details. TrackReadingWordAt hält einen eigenen Wortbox-Cache für die getrackte Seite vor, der beim Seitenwechsel automatisch neu aufgebaut wird, sodass die Kosten pro Grenze konstant bleiben. Großzügig begrenzt wird der Index aber nicht: Ein Index auf oder hinter der Zeichenzahl der Seite gibt −1 zurück, statt auf das letzte Wort zu klemmen. Behandeln Sie −1 als „vorige Hervorhebung halten“, nie als Fehler, denn Interpunktionsläufe und Zwischenwort-Leerraum erzeugen legitime Grenzen, die zu keinem Wort gehören. Wenn Sie jedes −1 loggen, ertrinken Sie darin; zählen Sie sie stattdessen pro Seite und untersuchen Sie Seiten, bei denen das Verhältnis springt. Meist weist das auf einen Textnormalisierungsfehler aus Regel eins hin.
Der Cursor selbst: Farbe, Folgen und Aufräumen
SetReadingWord malt die Hervorhebung direkt, wenn Sie die Wortbox selbst halten, ReadingWordColor gestaltet sie, und ReadingWordFollow := True scrollt die Ansicht gerade weit genug, damit das gesprochene Wort sichtbar bleibt. Diese letzte Eigenschaft ist wichtiger, als sie klingt: Ein selbstgebautes „aktuelles Wort zentrieren“-Scrollen lässt die Seite bei jedem Zeilenwechsel rucken, und bewegungsempfindliche Leser schalten die Funktion nach einer Minute ab. Die Hervorhebung rendert nur auf der Seite, die im aktiven TPdfView gerade angezeigt wird. Mehrseitiges Lesen muss deshalb PageNumber synchron zur Sprache weiterschalten und den Vorbereitungsschritt für die neue Seite erneut ausführen, bevor deren erstes Grenzereignis eintrifft, damit Sprachtext und Offsets zur frischen Seite passen.
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
Symmetrie zählt beim Beenden: Jeder Pfad für Pause, Stopp und Seitenwechsel muss mit ClearReadingWord enden. Der am häufigsten gemeldete „Bug“ in unserer Beta war ein bernsteinfarbenes Rechteck, das auf einer pausierten Seite stehen blieb — harmlos, aber jeder Tester meldete es.
Die Sprechgeschwindigkeit belastet diese Pipeline stärker als die Dokumentgröße. Bei 300 Wörtern pro Minute treffen Grenzereignisse alle 200 ms ein; bei den schnellsten SAPI-Raten schneller, als das Auge bequem verfolgen kann. Fassen Sie zusammen, statt zu stauen: Wenn eine neue Grenze eintrifft, während eine Hervorhebungsaktualisierung noch aussteht, verwerfen Sie die veraltete. Ein Cursor, der jedes Wort der Reihe nach besucht, aber eine halbe Sekunde zu spät ist, wirkt kaputt; einer, der gelegentlich ein Wort überspringt und synchron bleibt, nicht.
Grenzfälle, die Demos von Produkten trennen
Drei Kategorien kehren wieder. Kombinierende Zeichen: Unicode-Folgen wie Grundbuchstaben mit kombinierenden Diakritika können mehr Zeichenindizes belegen, als das sichtbare Wort vermuten lässt. Offset-Arithmetik, die einen Index pro sichtbarem Glyph annimmt, driftet deshalb — ein weiterer Grund, TrackReadingWordAt die Abbildung erledigen zu lassen, statt Wortnummern selbst zu berechnen. Silbentrennung: Ein über einen Zeilenumbruch getrenntes Wort wird zu zwei Boxen; sprechen Sie es als ein Token, dann löst das Grenzereignis für die zweite Hälfte auf die erste Box auf — akzeptabel, aber entscheiden Sie das bewusst. Und Tagged gegenüber untagged Documents: Die Wortreihenfolge folgt der logischen Struktur des Dokuments, wenn korrektes Tagging vorhanden ist, also dem Gebiet von ISO 14289, PDF/UA, und fällt sonst auf Layoutheuristiken zurück. Eine zweispaltige ungetaggte Seite kann daher quer über beide Spalten gelesen werden. Rotierte Seiten fügen einen vierten Fall hinzu: Das Rect jedes Wortes begrenzt es weiterhin korrekt im Seitenraum, aber eine Viewport-Follow-Strategie, die auf horizontalen Fluss abgestimmt ist, scrollt bei vertikal laufendem Text ruckartig. Halten Sie deshalb mindestens ein rotiertes Dokument im Regressionssatz. Für Leseordnungsbehandlung, satzbasierte Einheiten über ReadingUnits und den weiteren Assistive-Stack siehe einen barrierefreien PDF-Reader in Delphi bauen.
Ein Plattformhinweis: SAPI ist Windows-only. Die Wortbox- und Tracking-API ist unter Lazarus/FPC identisch, aber Linux- und macOS-Builds brauchen hinter denselben Grenzereignissen einen anderen Synthesizer. Die Setup-Unterschiede behandelt der Viewer unter Lazarus und FPC. Die Renderingkosten der Hervorhebung interagieren bei hohen Sprechgeschwindigkeiten auch mit dem Seiten-Cache; die Budgetrechnung in Render-Caching und Zoom-Performance gilt hier unverändert.
Häufige Fragen
Warum gibt TrackReadingWordAt immer −1 zurück?
Meist liegt es an einem von drei Gründen: Die übergebene Seitennummer ist außerhalb des Bereichs oder das Dokument ist nicht aktiv, der an die TTS-Engine gegebene Text unterscheidet sich vom extrahierten Seitentext und die Offsets passen deshalb nicht, oder der Zeichenindex gehört zum Leerraum zwischen Wörtern. Prüfen Sie sie in dieser Reihenfolge.
Warum aktualisiert sich die Hervorhebung nach einem Seitenwechsel nicht mehr?
Der Lesecursor zeichnet nur auf der aktuellen Seite der aktiven Ansicht. Setzen Sie PageNumber weiter und holen Sie PageWordBoxes für den Sprachtext neu, bevor Sie fortfahren, damit sich die Grenzoffets auf die jetzt sichtbare Seite beziehen.
Kann ich ganze Sätze statt einzelner Wörter hervorheben?
Ja — ReadingUnits liefert Satz- und Blockeinheiten mit eigenen Hervorhebungsrechtecken, die Sie mit SetReadingHighlight zeichnen. Das passt zu langsameren Hörern und reduziert visuelle Unruhe bei hohen Sprechgeschwindigkeiten.
Versionsanforderungen (v1.53 oder neuer für Wortboxen, v1.56 für den Tracking-Cursor), die vollständige Lese-API und eine funktionierende Vorlese-Demo stehen auf der Produktseite: PDFium Component.