مقال تقني

إنشاء قارئ PDF قابل للوصول في Delphi باستخدام PDFium

وجّه NVDA إلى عارض PDF مبني حديثاً في Delphi، وستحصل غالباً على إحدى نتيجتين: صمت كامل، أو نص يُقرأ بالترتيب الذي صادف أن خزّنه content stream: تذييل الصفحة أولاً، ثم العمود الأيمن، ثم العنوان الذي يفتح الصفحة بصرياً. التصيير قد يكون مثالياً؛ أما تجربة الاستماع فتصبح بلا قيمة. الفجوة موجودة لأن rasterization والقراءة مساران منفصلان: ترتيب الرسم داخل PDF content stream لا يلتزم بالترتيب الذي ينبغي أن يسمعه الإنسان. ولهذا يوفّر PDFium Component، وهو غلاف VCL/LCL حول محرك PDFium لـ Delphi وC++Builder وLazarus، عائلة مخصصة من واجهات القراءة تحديداً لأن واجهات التصيير لا تستطيع أداء هذه المهمة.

ثلاث مشكلات تحدد نجاح مشروع قارئ قابل للوصول: استخراج ترتيب قراءة صالح للنطق، إبقاء مؤشر كلمة مرئي متزامناً مع إخراج الكلام، والتراجع بصدق عندما لا يكون المستند موسوماً أصلاً. لكل واحدة مسار API واضح ونمط فشل واضح يستحق أن تعرفه قبل أن تكتب أول event handler.

ترتيب القراءة يعيش في شجرة البنية، لا في ترتيب الرسم

يعرّف ISO 32000-1 §14.8 البنية المنطقية كشجرة من عناصر البنية موضوعة فوق محتوى الصفحة، ويجعل PDF/UA (ISO 14289-1) هذه الشجرة إلزامية: يجب أن يكون كل محتوى حقيقي قابلاً للوصول عبرها وفق ترتيب القراءة، مع استبعاد artifacts. التقرير الموسوم جيداً يعرف أن "Quarterly Results" عنوان من المستوى الثاني، وأن جدول الإجماليات جدول له خلايا رؤوس. أما التقرير غير الموسوم فهو مجرد مسارات glyph موضوعة على الصفحة ولا شيء أكثر.

يمشي ReadablePageContent عبر هذه البنية عند وجودها، ويعيد أجزاء محتوى موسومة بحقل دلالي Kind مثل cfHeading وcfParagraph وقيم قريبة، بحيث يستطيع UI القارئ أن يعلن "heading" قبل النص بدلاً من قراءة سطر عريض كأنه متن عادي. وعندما تكون شجرة البنية غائبة أو غير صالحة، يتحول الاستدعاء نفسه إلى تحليل تخطيطي استدلالي: كشف الأعمدة، تجميع baseline، والترتيب من اليسار إلى اليمين. تكون النتيجة مفيدة غالباً في مستندات العمود الواحد، وغير موثوقة في النشرات والنماذج متعددة الأعمدة وكل ما يحتوي على sidebars. الانضباط الحاسم هو أن تخبر المستخدم أي حالة يواجهها؛ فالـ API يمنحك هذه الحقيقة مباشرة: سجل TPdfReadableContent المعاد يحمل حقل Source تكون قيمته rosStructure عندما جاء الترتيب من الشجرة الموسومة، وrosHeuristic عندما تم تخمينه من التخطيط. عرض ترتيب مخمّن كأنه ترتيب موثّق هو مكافئ الوصولية لعلامة نجاح خضراء على build لم يُختبر.

الطريقة العملية لتصنيف ملف عند الفتح هي فحص IsTagged وتشغيل ValidatePdfUa مرة واحدة مع تخزين الحكم. فشل PDF/UA لا يعني رفض المستند؛ بل يعني أن شريط الحالة يعرض "ترتيب قراءة مقدّر"، وأن فريق الدعم يعرف بالضبط ما ينظر إليه عندما يبلغ عميل عن سرد غير منطقي في ملف محدد.

من الصفحة إلى قائمة الكلام عبر ReadingUnits

العمود الفقري لـ text-to-speech هو 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 الفارغة على صفحة يظهر عليها محتوى بصرياً هي كاشف الصفحات الصورية فقط. الصفحة الممسوحة ضوئياً تحتوي على pixels لكنها لا تحتوي على طبقة نص، والاستجابة الصحيحة هي تحذير منطوق، "هذه الصفحة لا تحتوي على نص قابل للاستخراج"، لا صمت لا يستطيع المستخدم تمييزه عن تعطل.

مؤشر كلمة يتبع الصوت

الإبراز على مستوى الكتلة يشعر المستخدمين ضعاف البصر بالبطء عندما يتابعون بصرياً أثناء الاستماع. الإبراز على مستوى الكلمة، أو أسلوب "karaoke"، يحتاج إلى عنصرين: هندسة الكلمات، وخريطة من callbacks التقدم في محرك TTS إلى تلك الهندسة. يوفّر PageWordBoxes الهندسة كسجلات TPdfWordBox: نص الكلمة، وإزاحة الحرف، وعدد الأحرف، ومستطيل في إحداثيات الصفحة. ويوفّر TrackReadingWordAt الخريطة: يحوّل موضع حرف، وهو بالضبط ما يرسله SAPI في إشعار word-boundary، إلى فهرس داخل مصفوفة word-boxes، ويبرز الكلمة التي تحتويه في الاستدعاء نفسه.

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 بذاكرة word-boxes الخاصة به للصفحة المتتبعة، لذلك لا تحتاج إلى تغذيته مسبقاً، ولا يدخل التصيير في الأمر لأن word boxes تأتي من طبقة نص الصفحة، ما يعني أن خدمة كلام بلا واجهة يمكنها تتبع المواضع. صارم: يجب أن يشير فهرس الحرف إلى النص الذي استخرجه المكوّن. تعيد الدالة أيضاً -1 بدلاً من رفع استثناء عندما يشير CharIndex إلى ما بعد نهاية نص الصفحة، وهذا يحدث بانتظام عندما يطلق محرك TTS حدث boundary نهائياً لعلامات ترقيم لاحقة. تعامل مع -1 كأمر "امسح المؤشر"، لا كحالة خطأ.

على جانب العرض، يتحكم ReadingWordColor في إبراز المؤشر؛ اللون الكهرماني الافتراضي ينجو فوق معظم خلفيات الصفحات، لكن افحصه تحت كل display filter يقدمه العارض، لأن المؤشر الكهرماني قد يختفي بالكامل تحت قلب الألوان، وقلب الألوان مع الكلام هو بالضبط التركيبة التي يستخدمها ضعاف البصر. ضبط ReadingWordFollow إلى True يجعل العرض يمرر الكلمة المنطوقة إلى مجال الرؤية تلقائياً، وهو ضروري في الصفحات المكبرة متعددة الشاشات. قاعدة نطاق واحدة: SetReadingWord يرسم على صفحة TPdfView النشطة فقط، لذلك قرر هل يوقف تمرير المستخدم الكلام أم تنتصر متابعة المؤشر؛ عدم اختيار أي منهما يترك الكلام يعمل بينما المؤشر غير مرئي.

مستندات تقاوم التنفيذ الساذج

ثلاث فئات إدخال تكسر التطبيقات الساذجة كثيراً بما يكفي لتستحق عينات regression دائمة في test suite.

  • ملفات غير موسومة لكنها غنية بالنص. يكون الترتيب الاستدلالي صحيحاً غالباً في التقارير الخطية، وخاطئاً في التخطيطات ذات sidebars أو pull quotes. سمِّ الترتيب بأنه مقدّر في UI وفي سجل التشخيص.
  • مسوح صورية فقط. لا توجد طبقة نص على الإطلاق. اكشفها من وحدات القراءة الفارغة ووجّه المستخدم إلى خطوة OCR upstream بدلاً من ترك القارئ لا ينطق شيئاً.
  • أحرف مركبة ونصوص مختلطة. Unicode combining marks لا تتطابق دائماً واحداً لواحد مع الكلمات المرئية، لذلك قد يختلف عدد word-boxes عما يتوقعه tokenizer الخاص بك. لا تفهرس مصفوفة word-boxes بحسابات مشتقة من تقسيمك؛ استخدم فقط الفهارس التي يعيدها TrackReadingWordAt.

القبول: اختبر كمدقق لا كعرض تجريبي

عبارة "قرأ عينتي بصوت عال" ليست قبولاً. المرور القابل للدفاع يشغّل ثلاثة مستندات عبر build النهائي مع NVDA مرفقاً: ملف موسوم معروف، حيث تُعلن العناوين كعناوين ويُقرأ الجدول بترتيب الصفوف؛ ملف غير موسوم معروف، حيث يكون مؤشر الترتيب المقدّر مرئياً؛ ومسح ضوئي، حيث يُنطق تحذير صريح بعدم وجود نص.

بعد ذلك تحقق من أن مؤشر الكلمة يبقى ملتصقاً عند مضاعفة سرعة الكلام وخفضها إلى النصف، وأن تمرير ReadingWordFollow لا يتصارع مع التمرير اليدوي. أخيراً، بدّل كل مرشح لون أثناء تشغيل الكلام وتأكد من بقاء المؤشر مرئياً؛ يغطي مقال مرشحات الألوان لضعاف البصر ذلك المسار التصييري، ويتعمق شرح مؤشر الكلمة مع الكلام أكثر في تفاصيل توقيت TTS.

FAQ

هل يحتاج القارئ إلى PDF موسوم لكي يعمل أصلاً؟

لا. يعود ReadablePageContent وReadingUnits إلى تحليل تخطيطي استدلالي في الملفات غير الموسومة، ويخبرك حقل Source في المحتوى المقروء أي مسار أنتج الترتيب. العبء يقع على UI: ميّز ترتيب شجرة البنية الموثّق عن الترتيب المقدّر، لأنهما يفشلان بطرق مختلفة ويحتاج الدعم إلى معرفة أيهما موضوع الشكوى.

لماذا يعيد TrackReadingWordAt القيمة -1 في منتصف الصفحة؟

عادةً يكون فهرس الحرف من محرك TTS يشير إلى نص عالجته مسبقاً قبل وضعه في القائمة، أو وقع على مسافة بين الكلمات. يجب أن تشير الإزاحات إلى النص الذي استخرجه المكوّن، وهو النص نفسه الذي قسّمه PageWordBoxes، لا إلى نسخة منظفة منه.

هل يمكنني فحص امتثال الوصولية برمجياً؟

نعم؛ يعيد ValidatePdfUa مستوى المطابقة المكتشف مع مجموعة مخالفات PDF/UA لكل مستند، ويدمج BuildPdfPreflightReport الفحص نفسه داخل تقرير متعدد المعايير. إنه كاشف لا أداة إصلاح: استخدم الحكم لضبط توقعات المستخدم عند الفتح ولتصنيف الملفات الواردة.

واجهات reading-unit وword-box المعروضة هنا جزء من PDFium Component لـ Delphi وC++Builder (VCL) وLazarus/FPC (LCL). تربط صفحة المنتج مرجع API الكامل، بما في ذلك بنى السجلات لوحدات القراءة وword boxes المستخدمة في الأمثلة أعلاه.