مقال تقني

إعادة تدفق محتوى PDF إلى HTML متجاوب في Delphi

يُحوِّل PDFium Component ملف PDF ثابت التخطيط إلى نموذج دلالي قابل لإعادة التدفق، باستخدام BuildReflowDocument، ويُصدّر ذلك النموذج كـ HTML مستقل بذاته عبر ToHtml. تبقى العناوين عناوين، وتبقى عناصر القوائم عناصر قوائم، وتخرج الجداول المُكتشَفة في الصفحة كترميز جدول حقيقي مع الحفاظ على خلايا الترويسة والامتدادات. ولا يُشير أي شيء في المخرجات إلى سكربت أو ورقة أنماط خارجية

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

من أين تأتي المعلومات الدلالية؟

يبدأ كل شيء من GetStructuredText، المصدر الوحيد للنص والدلالة في المكوّن. وحين يحمل ملف PDF شجرة بنية، أي PDF موسوم كما هو مُعرَّف في البند 14.7 من ISO 32000-1، يتبع النموذج التسلسل الهرمي المنطقي الذي سجّله المُنتِج. وحين لا يحملها، وهو حال معظم ملفات PDF في الواقع، يعود النموذج إلى ترتيب التخطيط الفيزيائي المحسوب بالفعل لأغراض ترتيب القراءة

وهذا الاختيار يحافظ على حدّ صارم: لا يُدخَل مُحلِّل PDF ثانٍ ولا محرك رسم ثانٍ للإجابة عن أسئلة يستطيع الموجود الإجابة عنها. وآلية ترتيب القراءة الكامنة موصوفة في كتل النص البنيوية وترتيب القراءة، ونموذج إعادة التدفق طبقة دلالية فوقها لا بديل عنها

ويسجّل كل عقدة من أين أتت معلوماتها، بحيث يستطيع المستهلِك التمييز بين عنوان أعلنه المستند وعنوان استنتجته إرشادات التخطيط. وينبغي لخطوط الأنابيب الحساسة للثقة قراءة ذلك الحقل بدلًا من معاملة جميع العُقد كمتساوية في الموثوقية

شجرة مسطَّحة، ولماذا ليست شجرة كائنات

النموذج شجرة مسطَّحة بترتيب سابق: مصفوفة من العُقد حيث تحمل كل عقدة ParentIndex وDepth، بدلًا من سجل تكراري أو رسم بياني للكائنات بملكية. وتقيم الصفحات، والعناوين، والفقرات، والقوائم، وعناصر القوائم، والأشكال، والتسميات التوضيحية، والجداول، والصفوف، والخلايا جميعها في تلك المصفوفة الخطية الواحدة

وتترتب على ذلك فائدتان. يستطيع المستهلِكون بث المصفوفة بالترتيب من دون تكرارية، ما يجعل إصدار HTML أو Markdown أو عرض شجري حلقة بسيطة. ويبقى التخطيط قابلًا للنقل عبر Delphi وC++Builder وFree Pascal، التي تختلف في كيفية تعاملها مع الأنواع المُدارة التكرارية عبر حدود ABI. وسجل تكراري من مصفوفات ديناميكية هو بالضبط نوع البنية التي تُصرَّف في كل مكان وتتصرف بشكل مختلف قليلًا في كل منها

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // كتلة نمط مضمَّنة، لا ملف خارجي
    Options.MaxNodes := 200000;          // ميزانية تفشل مغلقة
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

كيف تُمنع الجداول من الظهور مرتين؟

يعمل اكتشاف الجداول بعد جمع النص البنيوي لصفحة، ما يخلق خطرًا واضحًا: محتوى الخلية نفسه موجود في كل من كتل النص والجدول المُكتشَف. وإصدار كليهما ينتج HTML يلي فيه كل جدول محتواه نفسه مجددًا كفقرات منفصلة

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

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

تصدير HTML يبقى مستقلًا بذاته

تجتاز ToHtml النموذج المبني بالفعل ولا تعود أبدًا إلى PDFium، بحيث لا يكلّف التصدير مرتين شيئًا إضافيًا ولا يمكن أن ينتج نتيجة مختلفة من النموذج نفسه. تُهرَب قيم النص والسمات بشكل موحّد، وتُقيَّد مستويات العناوين ضمن نطاق h1 إلى h6 الذي يُعرِّفه HTML فعليًا، وتمر خلايا الترويسة وRowSpan وColumnSpan كما كُتبت

وCSS الاختياري كتلة نمط مضمَّنة عادية. لا يوجد سكربت، ولا خط ويب، ولا أي مورد خارجي من أي نوع، وهذا ما يجعل المخرجات آمنة للتضمين في بريد إلكتروني، أو عارض مساعدة، أو عنصر تحكم متصفح معزول:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // إبقاء حدود الصفحات مرئية
  Options.PreserveLineBreaks := False;   // السماح للمتصفح بلفّ الفقرات

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

وPreserveLineBreaks هو الخيار الأجدر بالتفكير فيه. ففاصل السطر في PDF قرار تنضيد اتُّخذ لعرض صفحة ثابت، لذا فإن الحفاظ عليه على شاشة ضيقة يُعيد إنتاج المشكلة نفسها التي وُجدت إعادة التدفق لحلّها. احتفظ بالفواصل للشعر، وقوائم الكود، والعناوين البريدية؛ وأسقطها للنثر

الميزانيات، والإلغاء، وحالة الصفحة

لكل من الأحرف، والعُقد، والجداول، والخلايا سقف، ويُتحقَّق من كل منها قبل التخصيص لا بعده، بحيث يفشل مستند مشوَّه أو عدائي بنظافة بدلًا من استهلاك الذاكرة إلى أن يفعل شيء آخر ذلك. ويُتحقَّق من رمز الإلغاء عند حدود الصفحة، والكتلة، والجدول، والصف، والخلية، ما يُبقي مسحًا مُلغى لمستند من ألف صفحة سريع الاستجابة

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

ما الذي تصلح له إعادة التدفق، وما لا تصلح له

مخرجات إعادة التدفق مُدخَل ممتاز لفهرسة البحث، وعروض القراءة القابلة للوصول، والعرض على الهاتف المحمول، وترحيل المحتوى. وهي ليست محوّلًا يحافظ على الدقة: فالمواضع المطلقة، والخطوط الدقيقة، والأعمال الفنية المتجهة، وهندسة الصفحة الدقيقة كلها خارج غرضها بالتصميم. حين تحتاج مهمة إلى أن تبدو الصفحة كما هي، ارسمها؛ وحين تحتاج إلى أن تكون الصفحة قابلة للقراءة في مكان آخر، أعد تدفقها

وبالنسبة إلى تقنية الدعم تحديدًا، يقترن نموذج إعادة التدفق بميزات القراءة الموصوفة في بناء قارئ قابل للوصول، وتُنتج المستندات التي تحمل شجرة بنية حقيقية نماذج أفضل بشكل ملحوظ، وهو حجة جيدة للتحقق من الوسم في المنبع كما هو موصوف في التحقق من شجرة بنية PDF/UA

وتشترك إعادة التدفق، والنص البنيوي، والتحقق من الوسم، والرسم في كائن مستند واحد عبر Delphi وC++Builder وLazarus؛ وتُوصَف واجهة البرمجة الكاملة على صفحة PDFium Component لـ Delphi