مقال تقني

استخراج النصوص من ملف PDF محمل في دلفي باستخدام HotPDF

يستخرج مكون HotPDF نصوص Unicode من أي ملف PDF تقوم بتحميله في دلفي من خلال استدعاءين: ترجع ExtractLoadedPageText نص تدفق القراءة للصفحة، وتقوم ExtractLoadedPageTextLayout (المضافة في الإصدار v2.263.0) بإعادة بناء الترتيب المرئي للصفحة كنص عادي، بحيث تظل الأعمدة والمسافات البادئة ومحاذاة الجداول سليمة في المخرجات. وكلاهما يعمل على مستندات لم يتم إنشاؤها بواسطة HotPDF، وهي الحالة المهمة فعليًا: الفاتورة التي أرسلها إليك عميل عبر البريد الإلكتروني، أو التقرير الذي سلمه مكتب مسح ضوئي، أو العقد الناتج عن برنامج لا يمكن لأحد تسميته بعد الآن

طلب الوصول إلى هناك آليات أكثر مما يوحي به توقيعا الاستدعاء، لأن ملف PDF لا يخزن النصوص بالطريقة التي يخزن بها ملف نصي عادي. يستعرض هذا المقال كلا وضعي الاستخراج، ثم يكشف الغطاء عن الأجزاء الثلاثة الموجودة بالأسفل — قارئ CMap، ومترجم تدفق المحتوى، وسلسلة بدائل فك تشفير الخطوط — لأن معرفة كيفية عمل المطابقة يمثل الفارق بين تجاهل المخرجات غير المفهومة وتشخيص سببها

لماذا يعد استخراج النصوص أصعب من قراءة السلاسل النصية من الملف؟

يسجل تدفق محتوى PDF رموز الأحرف، وليس الأحرف نفسها. وتحمل المعاملات Tj و TJ (معيار ISO 32000-1 §9.4.3) سلاسل من البايتات التي يعتمد معناها بالكامل على الخط المحدد بواسطة Tf السابقة: فقد يمثل البايت 0x41 الحرف A تحت ترميز WinAnsi، أو شكلاً رمزيًا عشوائيًا في خط مجزأ، أو نصف CID ثنائي البايت في خط CJK مركب. ويحدد معيار ISO 32000-1 §9.10 استخراج النصوص على أنه مشكلة فك التشفير هذه بالضبط — وهي مطابقة كل رمز مرة أخرى إلى Unicode باستخدام أي معلومات يوفرها قاموس الخط — والمعيار صريح في أنه لا يُطلب من الملف المتوافق توفير معلومات كافية للقيام بذلك

تشرح تلك الفقرة الأخيرة كل تقرير خطأ مر بك يتساءل "لماذا ينتج عن النسخ واللصق من ملف PDF هذا نصوص غير مفهومة". فالمنتج الذي يضمن خطًا مجزأً بدون جدول /ToUnicode قد كتب ملفًا يعرض بشكل مثالي ويُستخرج كنصوص بلا معنى، لأن مطابقة الرموز بالرموز الرسومية موجودة ولكن مطابقة الرموز بـ Unicode لم تشحن أبدًا. وبالتالي فإن أي واجهة برمجة تطبيقات استخراج نزيهة هي سلسلة من الحلول البديلة التي تبذل قصارى جهدها، والسؤال المفيد هو إلى أي مدى تصل هذه السلسلة عمقًا

استخراج نصوص تدفق القراءة باستخدام ExtractLoadedPageText

لفهرسة البحث، أو مطابقة الكلمات المفتاحية، أو تغذية النصوص إلى خط تدفق التحليل، فإن ExtractLoadedPageText هو الاستدعاء الذي تريده. التوقيع هو: function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — فهارس الصفحات تبدأ من الصفر، وتصل النتيجة كـ UnicodeString أصلي في دلفي,وترجع الوظيفة القيمة False عندما لا تحتوي الصفحة على تدفق محتوى قابل للقراءة بدلاً من إثارة استثناء

var
  Pdf: THotPDF;
  PageCount, I: Integer;
  PageText, AllText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('invoice.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
      if Pdf.ExtractLoadedPageText(I, PageText) then
        AllText := AllText + PageText + #13#10;
    // AllText now holds the reading-flow text of the document
  finally
    Pdf.Free;
  end;
end;

تأتي فواصل الأسطر في المخرجات من خوارزمية بسيطة مقصودة: عندما يتحرك الأصل الرأسي لشكل رمزي بمقدار يزيد عن نصف حجم الخط الحالي — وهي علامة لخطوة Td أو T* في تدفق المحتوى — يتم إدراج سطر جديد. وتتحول الأحرف التي لا يستطيع فك التشفير حلها إلى مسافات بدلاً من الاختفاء، بحيث تنجو حدود الكلمات حتى عندما لا تنجو الأشكال الرمزية الفردية. وما لا يحاول هذا الوضع هو التجميع حسب ترتيب القراءة أو اكتشاف الأعمدة المتعددة: فالصفحة المكونة من عمودين تخرج متداخلة بترتيب تدفق المحتوى، وهو الترتيب المرئي عادةً ولكن ليس دائمًا

متى يجب استخدام الاستخراج الذي يحافظ على التخطيط بدلاً من ذلك؟

يعد ExtractLoadedPageTextLayout هو الاستدعاء المناسب كلما كان للموضع معنى: الجداول، والنماذج، وقوائم التعليمات البرمجية، وأي شيء تنوي مقارنته أو البحث فيه أو تحليله حسب العمود. وبدلاً من تسوية الأشكال الرمزية في دفق واحد، فإنه يجمعها في خطوط أساسية، ويرتب كل خط أساسي حسب المحور X، ويعيد إنتاج المساحات البيضاء الأفقية والرأسية على شبكة أحرف أحادية المسافة بحجم يعتمد على متوسط تقدم الشكل الرمزي وحجم الخط. وتصبح الفجوات الواسعة بين عمليات التشغيل على نفس الخط الأساسي سلاسل من المسافات؛ وتتحول الفجوات الكبيرة بين الخطوط الأساسية إلى أسطر فارغة. وتقرأ النتيجة تمامًا كما تبدو الصفحة

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Columns, indentation and table alignment survive as
  // spaces and blank lines on a character grid
end;

يشترك الوضعان في كل بايت من آليات فك التشفير ويختلفان فقط في كيفية ترتيب الأشكال الرمزية التي تم فك تشفيرها، وبالتالي فإن الاختيار لا يكلف شيئًا من حيث دقة النتائج. اختر ExtractLoadedPageText عندما تهمك الكلمات فقط، و ExtractLoadedPageTextLayout عندما يهمك الترتيب. ويظل اكتشاف ترتيب القراءة متعدد الأعمدة خارج نطاق التخطيط لكلا الوضعين — يعرض لك تمثيل الشبكة لصفحة مكونة من عمودين كلا العمودين جنبًا إلى جنب بأمانة، وهو أمر صحيح تمامًا للمقارنة ولكنه ليس كذلك لإعادة تدفق النصوص النثرية

كيف يقوم HotPDF بفك تشفير رموز الأحرف إلى Unicode؟

يحل مكون HotPDF كل رمز حرف من خلال سلسلة بدائل مرتبة حسب الأولوية: خريطة /ToUnicode CMap المضمنة في الخط أولاً، ثم إدخال /Encoding (تدفق أو خريطة CMap مسماة)، ثم — للخطوط المركبة — ملفات Adobe القياسية CMap لمجموعات الأحرف مثل Adobe-GB1 و Adobe-CNS1 و Adobe-Japan1 و Adobe-KR، وأخيرًا جداول WinAnsi و MacRoman المدمجة للخطوط البسيطة. والشرط الذي لا يمكنه تقديم إجابة يتراجع بصمت إلى الشرط التالي بدلاً من إثارة استثناء، والرمز الذي يستنفد السلسلة بأكملها يحل إلى 0 حتى يتمكن المستدعي من حساب مرات الفشل بدلاً من التخمين

تأتي خريطة /ToUnicode CMap (ISO 32000-1 §9.10.3) في المقدمة لأنها المطابقة التي كتبها المنتج خصيصًا للاستخراج. ويهُم مسار Adobe القياسي CMap مستندات CJK التي تستخدم خرائط CMap المحددة مسبقًا مثل UniGB-UTF16-H بدلاً من تضمين أي شيء: يشحن HotPDF ملفات المجموعات تحت دليل resources\CMap الخاص به، ويحدد موقعها بالنسبة للملف التنفيذي عند التشغيل، ويقوم بتخزين كل خريطة محللة مؤقتًا لكل عملية — وهو أمر جدير بالمعرفة لأن أكبرها، وهي خريطة Adobe-GB1، تحتوي على ما يقرب من 2 ميجابايت من النص مصدر الذي لا تريد إعادة تحليله لكل صفحة. وإذا كان الدليل غائبًا، فإن فك التشفير يتخطى ببساطة خرائط CMap المدعومة بالقرص ويعمل مع الجداول المضمنة بالإضافة إلى الترميزات المدمجة. وهذا هو مرآة جانب القراءة لمشكلة التشكيل التي تمت تغطيتها في تشكيل نصوص النص المعقدة باستخدام HotPDF، حيث تتم مواجهة نفس التمييز بين الرمز والشكل الرمزي في وقت الكتابة

فخان في بناء جملة CMap جديران بالمعرفة

تبدو ملفات CMap قابلة للتحليل بسهولة وهي ليست كذلك، وهناك تفصيلان يفسران معظم إخفاقات المحلل في المحاولة الأولى. الأول هو أن عدد السجلات يأتي قبل الكلمة المفتاحية للقسم: فيقرأ القسم 2 beginbfchar، وليس beginbfchar 2. فالمحلل الذي يتوقع العدد بعد الكلمة المفتاحية يستهلك الرقم كرمز عشوائي، ثم يجد صفر إدخالات في كل قسم. والنهج القوي — وهو النهج الذي استقر عليه قارئ HotPDF — هو تجاهل العدد بالكامل والتكرار حتى الكلمة المفتاحية المطابقة endbfchar / endbfrange، وهو ما يمتاز بتسامحه مع الملفات الواقعية التي تكون أعدادها خاطئة ببساطة

الفخ الثاني هو أن أهداف bfchar و bfrange هي سلاسل نصية بترميز UTF-16BE، وليست أعدادًا صحيحة. فالوجهة <D83DDE00> تعني U+1F600 — وهو زوج بديل يجب إعادة دمج في نقطة رمز واحدة — وقراءة تلك البايتات الأربعة كعدد صحيح بأسلوب الطرف الأكبر (big-endian) تنتج قيمة بلا معنى لكل نقطة رمز خارج المستوى متعدد اللغات الأساسي. ولم تعد الرموز التعبيرية في ملفات PDF غريبة، لذا فإن وحدة فك التشفير التي تتخطى إعادة دمج البدائل تفشل في الملفات التي يمتلكها مستخدموك بالفعل. يحلل HotPDF قيم الست عشرية الحرفية إلى بايتات خام أولاً، ثم يعيد دمج وحدات رمز UTF-16BE، وهو ما يغطي أيضًا الأهداف متعددة الأحرف التي تنتجها مخططات الوصلات (ligatures)

النزول إلى مستوى الأشكال الرمزية باستخدام ExtractLoadedPageGlyphs

كلا استدعائي النص مبنيان على ExtractLoadedPageGlyphs، ومصفوفة THPDFGlyphArray الأساسية متاحة للكود الخاص بك أيضًا. ويحمل كل THPDFGlyphRecord نقطة رمز Unicode المحلولة إلى جانب رمز الحرف الخام، وعرض البايت الخاص بالرمز (1 أو 2 أو 4، ويتم تحديده بواسطة codespacerange لخريطة CMap)، ومفتاح مورد الخط النشط وحجمه، وأصل X و Y في مساحة المستخدم، والتقدم الأفقي. ويكفي ذلك لبناء اكتشاف حدود الكلمات، أو التمييز الموضعي، أو خوارزمية تخطيط مخصصة دون لمس تدفق المحتوى بنفسك

var
  Glyphs: THPDFGlyphArray;
  I, Unresolved: Integer;
begin
  if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
  begin
    Unresolved := 0;
    for I := 0 to High(Glyphs) do
      if Glyphs[I].Unicode = 0 then
        Inc(Unresolved);
    if Unresolved > 0 then
      ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
        [Unresolved, Length(Glyphs)]);
  end;
end;

يعد عد سجلات Unicode = 0، كما هو موضح أعلاه، هو الطريقة النزيهة لقياس جودة الاستخراج في مستند معين قبل الوثوق بالنص في المراحل اللاحقة. وتربط سجلات الأشكال الرمزية أيضًا كل حرف بالمعامل المصدر في تدفق المحتوى، وهو ما يجعل البحث عن النص واستبداله في المستندات المحملة في HotPDF ممكنًا فوق نفس الأساس

ما هي ملفات PDF التي لن تتخلى عن نصوصها؟

تهزم بعض الملفات أي مستخرج نصوص، ومن الأفضل اكتشافها بدلاً من شحن مخرجاتها. وتعد المستندات الممسوحة ضوئيًا هي الحالة الأكثر وضوحًا: فالصفحة التي تمثل صورة كبيرة واحدة لا تحتوي على معاملات نصية على الإطلاق، وبالتالي فإن الاستخراج يرجع بشكل صحيح سلسلة فارغة — والحل هو التعرف الضوئي على الحروف (OCR)، و استخراج صور الصفحة من ملف PDF المحمل هو الخطوة الأولى في هذا الخط التدفق. وتعد الخطوط المجزأة بدون جدول /ToUnicode هي الحالة الأصعب: فإذا كان مسار /Encoding وخرائط CMap القياسية فارغين أيضًا، فإن تلك الأشكال الرمزية تحل إلى 0 وتظهر كمسافات في استدعاءات النصوص. وتُستخرج المستندات المشفرة بشكل طبيعي بشرط تحميلها بكلمة المرور الخاصة بها عبر التحميل الزائد لـ LoadFromFile، بحيث يتم فك تشفير التدفقات قبل أن يراها المترجم

هناك حد أضيق يستحق التصريح به بوضوح: تقرأ سلسلة فك التشفير تدفقات CMap والمحتوى عبر مسار Flate الخاص بـ HotPDF، لذا فإن الخط الذي يستخدم تدفق ToUnicode الخاص به مرشحًا غير معتاد يتراجع إلى الاستراتيجية التالية بدلاً من إفشال الصفحة. وعملياً، يغطي FlateDecode كل شيء تقريباً تم إنتاجه في العقدين الأخيرين، والتراجع صامت حسب التصميم — فتكسب أفضل نص يسمح به الملف بدلاً من استثناء. ونفس آليات الكائنات في جانب القراءة التي تحل قواميس الخطوط هنا تدعم أيضًا تعديل البيانات الوصفية في المستندات المحملة، بحيث يمكن لخط تدفق استيعاب المستندات الاستخراج والفحص والتعليق في تمريرة واحدة

يعد استخراج النصوص، والتمثيل الذي يحافظ على التخطيط، والوصول على مستوى الأشكال الرمزية، وميزات البحث واستبدال المبنية عليها كلها جزءًا من مكون HotPDF القياسي لدلفي و C++Builder — بدون مكتبات DLL خارجية، وبدون خدمات نصوص نظام التشغيل، فقط كود Object Pascal يمكنك تتبعه خطوة بخطوة عندما يستقر ملف غريب في طابورك