מאמר טכני

בניית קורא PDF נגיש ב-Delphi עם PDFium

משתמש עיוור פותח דוח רבעוני במציג ה-Delphi החדש שלך, מפעיל את NVDA ושומע את כותרת התחתית של הדף, אחר כך עמודת מספרים, ואחר כך את הכותרת שכל קורא רואה היה קורא ראשון. או לא שומע כלום. הדף נראה מושלם על המסך, וזה בדיוק המלכוד: עיבוד וקריאה הם בעיות שונות הנפתרות בקוד שונה. הסדר שבו PDF מציג את הגליפים שלו אינו מחויב להתאים לסדר שבו אדם אמור לשמוע אותם, כך שמציג שנבנה רק על קריאות עיבוד מייצר תמונה מושלמת ונרטיב שאינו שמיש. PDFium Component, עטיפת ה-VCL/LCL סביב מנוע PDFium עבור Delphi, C++Builder ו-Lazarus, כוללת מערכת API נפרדת לקריאה מסיבה זו. ממשקי ה-API של ציור אינם יכולים לשחזר סדר קריאה שלא ניתן להם מלכתחילה

קורא נגיש עומד או נופל על שלושה דברים. הוא חייב לחלץ סדר שקורא מסך יכול להכריז, לשמור סמן מילים גלוי מקובע למה שהקול אומר ברגע זה, ולהודות כשמסמך לא תויג מעולם במקום לנחש ולהעמיד פנים. לכל אחד מהם יש ממשק API ברור לפנות אליו וכישלון שעוקץ אם תדלג על הפרט

סדר הקריאה נמצא בעץ המבנה, לא בסדר הציור

ISO 32000-1 §14.8 מגדיר מבנה לוגי כעץ אלמנטים המונח מעל תוכן הדף. PDF/UA (ISO 14289-1) מרחיק לכת ועושה את העץ הזה חובה: כל תוכן אמיתי חייב להיות נגיש דרכו בסדר קריאה, כשארטיפקטים של הדף מסומנים ככאלה ומדולגים. דוח מתויג כהלכה יודע ש"תוצאות רבעוניות" הוא כותרת רמה שנייה ושרשת הסיכומים היא טבלה עם תאי כותרת. דוח ללא תיוג הוא ערמה של ריצות גליפ ממוקמות שנראות כמו מסמך

ReadablePageContent עובר על המבנה כשהוא קיים ומחזיר קטעים מתויגים עם Kind סמנטי, ערכים כמו cfHeading ו-cfParagraph, כך שממשק המשתמש יכול להגיד "כותרת" לפני המילים במקום לקרוא שורה מודגשת כטקסט רגיל. ללא עץ שמיש, אותה קריאה חוזרת לניתוח פריסה היוריסטי: זיהוי עמודות, אשכול קווי בסיס, סדר משמאל לימין ומלמעלה למטה. הגיבוי הזה בסדר למזכר בעמודה אחת ורועש לניוזלטר, טופס רב-עמודות, כל דבר עם סרגל צד או ציטוט. מה שחשוב הוא לדעת איזה תוצאה קיבלת, וה-API אומר לך במפורש. הרשומה TPdfReadableContent נושאת שדה Source שמוגדר ל-rosStructure כשהסדר הגיע מהעץ המתויג, או rosHeuristic כשהוא הוסק מגאומטריה. הצגת סדר מנוחש כאילו אומת ושלחת גרסת הנגישות עם תג מעבר על בנייה שאף אחד לא הריץ

הצעד הזול בזמן פתיחה הוא לקרוא IsTagged ולקרוא ל-ValidatePdfUa פעם אחת, ואז לשמור בזיכרון. בדיקת PDF/UA שנכשלת אינה עילה לדחות את הקובץ. זו עילה לשים "סדר קריאה משוער" בשורת הסטטוס, כך שכשלקוח שולח תלונה על נרטיב מבולבל, התמיכה כבר יודעת אם הם מסתכלים על בעיית תיוג בקובץ או באג בקוד שלך

מדף לתור דיבור עם ReadingUnits

לצורך המרת טקסט לדיבור, ReadingUnits עושה את העבודה הכבדה. הוא מחזיר מערך של רשומות TPdfReadingUnit עבור הדף הפעיל, כל אחת מחזיקה את הטקסט לדיבור, את תפקידו הסמנטי ואת המלבנים שמאתרים אותו בדף. יש לו עמית ברמת המסמך, DocumentReadingUnits, כשרוצים קריאה רציפה על פני דפים. יחידה אחת נכנסת ישירות לחריץ אחד בתור הדיבור:

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;

שתי דברים בלולאה הזו קל לטעות בהם. שמור את התור לכל דף ובנה אותו מחדש בכל פעם שהמשתמש מנווט, כי יחידות קריאה נושאות מלבנים במרחב הדף; תור שנשאר מדף שלוש יצייר את ההדגשות שלו על דף ארבע. והתייחס למערך Units ריק בדף שבבירור יש בו תוכן כאל גלאי התמונה-בלבד שלך. דף סרוק הוא פיקסלים ללא שכבת טקסט מתחת, והתשובה הנכונה היא להכריז אזהרה ("לדף זה אין טקסט שניתן לחלץ") ולא לשתוק בדרך שהמאזין אינו יכול להבדיל ממתלה

סמן מילים שעוקב אחר הקול

הדגשת פסקה שלמה בבת אחת מרגישה עמומה למשתמש עם לקות ראייה שעוקב אחר המילים בעיניו בזמן שהן נקראות בקול. הדגשה ברמת מילה, אפקט הקריוקי, צריכה שני חלקים: הגאומטריה של כל מילה ודרך למפות את דוחות ההתקדמות של מנוע TTS לגאומטריה הזו. PageWordBoxes נותן לך את הגאומטריה כרשומות TPdfWordBox, כל אחת עם טקסט המילה, הסטת התו שלה, ספירת התווים שלה ומלבן במרחב הדף. TrackReadingWordAt נותן לך את המיפוי. הזן לו את מיקום התו שאירוע גבול-מילה של SAPI כבר מדווח, והוא ממיר את ההסטה הזו לאינדקס במערך תיבות-המילים ומצייר את הסמן על המילה המתאימה בקריאה אחת

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;

החוזה נדיב בנקודה אחת ונוקשה בנקודה אחרת. הנדיב: TrackReadingWordAt שומר את מטמון תיבות-המילים שלו לדף שהוא עוקב, כך שאין מה לטעון מראש, ואין עיבוד כלל כי תיבות המילים מגיעות משכבת הטקסט. שירות דיבור ללא ראש ללא חלון גלוי עדיין יכול לעקוב אחר מיקומים. הנוקשה: אינדקס התו חייב להצביע לתוך הטקסט שהרכיב חילץ, לא לתוך מחרוזת מנוקה שבנית בעצמך. כש-CharIndex עובר את סוף טקסט הדף, הפונקציה מחזירה -1 במקום להעלות חריגה, מה שקורה כל הזמן כשמנוע TTS מפעיל אירוע גבול אחרון עבור פיסוק אחרון. קרא ל--1 כ"נקה את הסמן", אל תקרא לו שגיאה

בצד התצוגה, ReadingWordColor מגדיר את צבע הסמן. ברירת המחדל של ענבר מחזיקה מעמד על רוב רקעי הדף, אך בדוק אותה תחת כל מסנן תצוגה שהמציג שלך מציע. סמן ענבר יכול להיעלם לחלוטין תחת היפוך צבעים, והיפוך שרץ לצד דיבור הוא בדיוק איך משתמש עם לקות ראייה עובד, כך שהשילוב היחיד שהכי צריך לעבוד נכון הוא זה שהדגמה מהירה לעולם לא בודקת. הגדר את ReadingWordFollow ל-True והתצוגה גוללת את המילה הנאמרת לתוך הראייה מעצמה, מה שאי אפשר בלעדיו בדף מוגדל שנשפך על פני מסכים. שים לב לכלל היקף אחד: SetReadingWord מצייר רק על דף ה-TPdfView הפעיל. החלט מראש אם גלילה ידנית משהה את הדיבור או שהתנהגות העקיבה עוקפת אותה, כי אי-בחירה בשניהם משאירה את הקול קורא בזמן שהסמן יושב איפשהו מחוץ למסך

המסמכים ששוברים את הקורא שלך

קומץ צורות קלט מביסות יישום תמים בצורה מספיק עקבית כדי שיהיו כדוגמאות קבועות בסוויטת הרגרסיה, לא כבאגים חד-פעמיים שאתה מתקן ושוכח

  • קבצים עשירים בטקסט אך ללא תיוג. סדר היוריסטי נוטה להיות נכון לדוח לינארי ושגוי ברגע שסרגל צד או ציטוט נכנסים. סמן את הסדר כמשוער, הן בממשק המשתמש והן ביומן האבחון שלך, כדי שהכישלון יהיה קריא מאוחר יותר
  • סריקות תמונה בלבד. אין שכבת טקסט כלל. תפוס אותן דרך יחידות קריאה ריקות והפנה את המשתמש לשלב OCR במעלה הזרם במקום לאפשר לקורא לנרט דף ריק
  • תווים משולבים וסקריפטים מעורבים. סימני שילוב Unicode לא תמיד קורסים אחד לאחד למילים חזותיות, כך שספירת תיבות-המילים יכולה לסטות ממה שה-tokenizer שלך מצפה. אל תאנדקס את מערך תיבות-המילים עם הסטות שחישבת על ידי פיצול הטקסט בעצמך; השתמש רק באינדקסים ש-TrackReadingWordAt מחזיר

בדוק כמבקר, לא כמדגים

"הוא קרא את הדוגמה שלי בקול" לא מוכיח כלום. מעבר שאפשר להגן עליו מריץ שלושה קבצים דרך הבנייה הגמורה עם NVDA מחובר: קובץ מתויג ידוע, שבו כותרות מוכרזות ככותרות וטבלה נקראת בסדר שורות; קובץ לא מתויג ידוע, שבו אינדיקטור הסדר המשוער גלוי; וסריקה, שבה אזהרת ללא-טקסט אכן נאמרת. כל אחד מבצע נתיב שהמקרה המאושר מדלג עליו

מכאן, אשר שסמן המילים נשאר נעול על קצב דיבור כפול ועל מחצית, ושגלילת ReadingWordFollow אינה מתמודדת עם גלילת המשתמש עצמו. אחר כך הרץ דיבור בזמן שאתה עובר על כל מסנן צבע וצפה שהסמן לא נעלם. מאמר מסני הצבע ללקות ראייה מכסה את נתיב העיבוד הזה בפירוט, וניתוח עמוק של סמן הדיבור ברמת מילה מפרק את תזמון ה-TTS

ממשקי ה-API של יחידת הקריאה ותיבות המילים המשמשים לעיל מגיעים עם PDFium Component עבור Delphi ו-C++Builder (VCL) ו-Lazarus/FPC (LCL). דף המוצר מקשר לפניית ה-API המלאה, כולל פריסות הרשומות עבור יחידות קריאה ותיבות מילים מאחורי הדוגמאות הללו