مقال تقني

استكمال جداول واعٍ بالمحتوى عبر صفحات PDF في دلفي

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

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

لماذا يفشل فحص هوامش الصفحة على صادرات Word؟

يفشل فحص هوامش الصفحة لأن معالج النصوص يوقف تخطيط الصفوف عند الهامش السفلي لا عند حافة الورقة. بـ ContinuationMargin الافتراضي البالغ 36 نقطة، كان يشترط النوع الأصلي أن تقع الحافة السفلى للشظية السابقة في حدود 36 نقطة من أسفل الصفحة وأن تقع الحافة العليا للشظية اللاحقة في حدود 36 نقطة من أعلى الصفحة. مستند مصدَّر من Word بهوامشه الافتراضية ذات البوصة الواحدة يضع الصف الأخير على بعد 72 نقطة على الأقل فوق أسفل الصفحة، وأبعد إن وُجد تذييل، فلا يتحقق الشرط قط. كان كل جدول طويل في مثل ذلك المستند يعود شظايا مستقلة بـ ContinuationGroup مساويًا صفرًا، وكان المستدعي يعود إلى الخياطة يدويًا. ما يزال الفحص منطقيًا لما صُمم حوله: التقارير المولدة بمحركات تخطيط تملأ الصفحة إلى صندوق محتوى ثابت وتبدأ الصفحة التالية ملاصقة لأعلاها. إنه ليس قاعدة سيئة بل قاعدة ناقصة، ولهذا أبقى الإصدار 3.117.0 عليها وأضاف مسارًا ثانيًا بدل استبدالها

ماذا يفحص الاختبار الواعي بالمحتوى بدلًا منه؟

يفحص الاختبار الواعي بالمحتوى هل يحتل شيئًا غير الجدول المساحة بين الشظيتين، مستخدمًا صناديق كلمات كل صفحة لا هندسة الصفحة. أثناء مشي ExtractDocumentTables على المستند يسجل، لكل صفحة، أدنى حافة سفلية لأي كلمة يقع أعلاها فوق شريط التذييل وأعلى حافة عليا لأي كلمة يقع أسفلها تحت شريط الرأس. كلا الشريطين عمقهما ContinuationMargin نقطة، فيؤدي الخيار نفسه الآن عملًا مزدوجًا: سماحية حافة صفحة وارتفاعًا لمنطقتي الرأس والتذييل الجاريين. تجتاز شظية الاقتران حين تكون الحافة السفلية للسابقة عند أدنى نص متني في صفحتها أو تحته، والحافة العليا للّاحقة عند أعلى نص متني في الصفحة التالية أو فوقه، كلٌّ ضمن AlignmentTolerance. بعبارة صريحة: كان الجدول آخر شيء في الصفحة N وأول شيء في الصفحة N+1، ورقم صفحة أو عنوان مستند في شريط الهامش لا يُحتسب. ذلك الاستبعاد ليس اعتباطيًا. يصنف ISO 32000-1 §14.8.2.2 الرؤوس والتذييلات الجارية بوصفها مخلفات ترقيم صفحات، محتوى وُجد بسبب فاصل الصفحة لا رغمًا عنه، والفكرة نفسها التي تجعل قارئًا موسومًا يتخطاها هي التي تجعل جدولًا يواصل المرور عبرها. تغطي مقالة المحتوى الموسوم كيف تعلن الملفات الموسومة تلك المخلفات صراحةً؛ هنا يُستنتج التصنيف من الموضع، لأن معظم الجداول المصدَّرة لا تحمل وسومًا أصلًا

لماذا يحتاج إكمال جداول مكوّن PDFium فحصين: بهوامش Word ذات البوصة يطالب فحص الهامش بحواف شظايا داخل نوافذ 36 pt لا يبلغها التخطيط قط، بينما يقارن الفحص الواعي بالمحتوى صناديق الكلمات ويربط حين يكون الجدول آخر محتوى متني في الصفحة N وأولها في الصفحة N+1، مغضيًا عن شريطي الرأس والتذييل الجارين
أيٌّ من الفحصين يفتح البوابة، وعندها فقط تشتغل الفحوص الباقية: صفحات متجاورة، ولا صف عنوان كامل العرض على الشظية اللاحقة، وحدود أعمدة متطابقة ضمن ضعفي AlignmentTolerance

يتحد الفحصان بـ OR. تقرير محرك تخطيط تمتد جداوله إلى حافة الورقة يجتاز الأول؛ وتصدير Word تتوقف جداوله عند الهامش يجتاز الثاني؛ ومستند يفعل الاثنين يجتاز مرتين. وفقط بعد نجاح أحدهما تشتغل البوابات الباقية، وتشتغل بترتيب ثابت: يجب أن تكون أرقام الصفحات متجاورة، وألا تفتح الشظية اللاحقة بصف عنوان، وأن تطابق حدود الأعمدة ضمن ضعفي AlignmentTolerance، أي 6 نقاط بالافتراضيات. التعداد هو TPdfTableContinuation بالقيم ptcNone وptcStart وptcMiddle وptcEnd. الشظية التي وُسمت ptcEnd ثم ارتبطت عابرًا إلى صفحة أخرى تُرقى إلى ptcMiddle، فيقرأ جدول من ثلاث صفحات start ثم middle ثم end بترتيب الصفحات. أرقام المجموعات تبدأ من 1 والصفر يعني غير مرتبط، ويصدر ToJson المعلومة نفسها عضويَّ continuation وcontinuationGroup، وهي الصيغة المفضلة إذا كانت خدمة لاحقة هي التي تخيط

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'itinerary-from-word.pdf';
    Pdf.LoadDocument;

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectContinuations := True;     // الافتراضي؛ ذُكر للتوضيح
    Options.ContinuationMargin := 54;        // تذييل بسطرين، بعمق ~50 pt تقريبًا

    Tables := Pdf.ExtractDocumentTables(Options);
    for I := 0 to High(Tables) do
      case Tables[I].Continuation of
        ptcStart:
          Writeln(Format('group %d starts on page %d (%d rows)',
            [Tables[I].ContinuationGroup, Tables[I].PageNumber,
             Tables[I].RowCount]));
        ptcMiddle, ptcEnd:
          Writeln(Format('group %d continues on page %d (%d rows)',
            [Tables[I].ContinuationGroup, Tables[I].PageNumber,
             Tables[I].RowCount]));
      else
        Writeln(Format('standalone table on page %d (%d rows)',
          [Tables[I].PageNumber, Tables[I].RowCount]));
      end;
  finally
    Pdf.Free;
  end;
end;

كيف يمنع صف العنوان اندماج جدولين؟

شظية صفحة تالية أول صفها خلية واحدة تمتد على كل الأعمدة تعامل بوصفها جدولًا جديدًا، لا بوصفها بقيّة سابقتها قط. هذه القاعدة موجودة لأن الفحص الواعي بالمحتوى وحده يربط بحماس زائد. الحالة التي كشفته كانت نموذجًا من طراز المحاضر: ينتهي جدول قرب أسفل الصفحة 1، ويبدأ جدول ثانٍ بعرض أعمدة مطابق قرب أعلى الصفحة 2، ولا يجلس بينهما شيء سوى التذييل، وتتطابق الأعمدة إلى النقطة. تحت فحص الهامش لم يلتقِ الاثنان قط لأن لا واحد منهما لمس حافة؛ وتحت الفحص الواعي ارتبطا فورًا، وصار نموذج بأقسام شبكة واحدة غير مترابطة. ما يفصل بينهما ظاهر في بنية الخلايا. يفتح الجدول الثاني بعنوان قسم مثل «RECIPIENT INFORMATION» مرسومًا خلية مدموجة واحدة بعرض كامل، والإكمال الحقيقي لا يفعل ذلك قط، لأن العنوان يعود إلى الجدول الذي بدأ أصلًا في الصفحة السابقة. يرمز TableStartsWithCaptionRow إلى ذلك بالضبط: الشظية فيها عمودان على الأقل وتحوي خلية بـ RowIndex = 0 وColumnIndex = 0 وColumnSpan = ColumnCount. يشتغل الفحص على الشظية اللاحقة فقط، فجدول صف عنوانه الخاص يجلس على صفحته الأولى لا يتأثر؛ العنوان في الصفحة N، ولا تُفحص إلا شظية الصفحة N+1

بوابة صف العنوان في مكوّن PDFium: يفتح الإكمال الحقيقي بخلايا بيانات ويلتحق بالمجموعة ContinuationGroup نفسها، بينما تُرفض شظية لاحقة يحمل صفها صفر خلية مدموجة واحدة بـ RowIndex 0 وColumnIndex 0 وColumnSpan يساوي ColumnCount بوصفها إكمالًا وتُبلَّغ جدولًا جديدًا
الفحص يلمس الشظية اللاحقة فقط، فجدول صف عنوانه الخاص يجلس على صفحته الأولى لا يتأثر، وتشتغل البوابة بعد أن يربط أحد الفحصين الحافيين الزوجًا سلفًا

مقارنة الأعمدة التي تليه، TablesHaveMatchingColumns، أكثر صرامة من «عدد الأعمدة نفسه». تعيد بناء مواضع حدود كل شظية من مستطيلات الخلايا، وتستبين الحدود التي تحجبها الخلايا المدموجة، وترفض الزوج عند انجراف أي حد أكثر من السماحية. لذلك يبقى جدولان رباعا الأعمدة بمقاييس مختلفة متباعدين حتى لو اصطف كل شيء آخر

ماذا يحدث لصف واحد يسفح إلى الصفحة التالية؟

الشبكة المحدودة التي تحمل صفًا واحدًا إلى الصفحة التالية تُكشف الآن وتربط، شريطة أن ينتهي بها المطاف في سلسلة إكمال؛ وحدها تُرمى. وُجد الافتراض MinRows البالغ 2 كي لا يُبلَّغ عن زوج خطين طائش بوصفه جدولًا، لكن الصف الأخير المدفوع عابرًا الفاصل صف حقيقي أسقطته أرضية صلبة قيمتها 2 بصمت، وبدا بقية الجدول مكتملة وهي ليست كذلك. يعالجه المسح على مستوى المستند في ثلاث خطوات. حين تضبط DetectContinuations وDetectRuledTables معًا، يشغّل مرور كل صفحة كاشف الشبكات المحدودة بأرضية الصفوف مخفضة مؤقتًا إلى 1، ولهذا يقبل ExtractTables الآن MinRows بقيمة 1 للشبكات المحدودة بينما يحفظ كشف المسافات البيضاء أرضية داخلية 2. ثم تُوسم الإكمالات على النتيجة الكاملة. بعدها تُنزع كل شبكة أقصر من MinRows الخاص بالمستدعي وليست جزءًا من أي سلسلة. تنجو شظية الصف الواحد فقط لأنها ارتبطت، وتُرشّح شبكة ذات صف واحد في وسط صفحة عادية تمامًا كما قبل

كيف يحفظ مكوّن PDFium صفًا محدودًا يسفح عابرًا فاصل صفحة: يشتغل مرور الشبكات المحدودة لكل صفحة بأرضية صفوف تساوي واحدًا حين تضبط DetectContinuations وDetectRuledTables، وتوسم الإكمالات على النتيجة الكاملة، ولا تنزع إلا الشظايا الأقصر من MinRows الجاثمة خارج كل سلسلة
ينجو الصف السافح لأن سلسلته تربطه، بينما تُرشّح شبكة صف واحد مستقلة في صفحة عادية تمامًا كما قبل، وتحفظ جداول كشف المسافات البيضاء أرضية صفوفها ذات الصفين بلا مثل هذا الإعفاء
// أعِد بناء كل سلسلة CSV واحدًا، نازعًا صفوف الرأس المكررة
// على شظايا الإكمال
procedure ExportChains(const Tables: TPdfTables; const Folder: string);
var
  I, R: Integer;
  Lines: TStringList;
  Csv: TStringList;
begin
  Csv := TStringList.Create;
  Lines := TStringList.Create;
  try
    for I := 0 to High(Tables) do
    begin
      if Tables[I].Continuation in [ptcNone, ptcStart] then
        Csv.Clear;
      Lines.Text := string(Tables[I].ToCsv);
      if (Tables[I].Continuation in [ptcMiddle, ptcEnd]) and
         (Lines.Count > 1) and (Tables[I].RowCount > 1) then
        Lines.Delete(0);            // رأس كرره معالج النصوص
      for R := 0 to Lines.Count - 1 do
        Csv.Add(Lines[R]);
      if Tables[I].Continuation in [ptcNone, ptcEnd] then
        Csv.SaveToFile(Format('%s\page%d-group%d.csv',
          [Folder, Tables[I].PageNumber, Tables[I].ContinuationGroup]));
    end;
  finally
    Lines.Free;
    Csv.Free;
  end;
end;

تفصيلان في ذلك الروتين متعمدان. لا يُنزع السفح ذو الصف الواحد قط، لأن الحارس على RowCount يحفظه، ومعالج نصوص يكرر صف الرأس في كل صفحة ينتج شظية أول سطر فيها الرأس من جديد، فإسقاط السطر صفر على شظايا middle وend صواب لذلك الحال وخطأ لمولد لا يكرر الرؤوس. افحص مستندًا واحدًا قبل أن تطلق الروتين على مجلد

أين ما تزال القواعد تتوقف

الفحص الواعي بالمحتوى لا يساوي إلا طبقة النص التي يقرؤها. على صفحة ممسوحة بلا نص إطلاقًا ترجع أقصيات النص المتني المسجلة إلى حدود الصفحة، ويتحقق شرط «لا شيء بينهما» فراغًا، وتبقى بوابتا صف العنوان والأعمدة فقط؛ تُوجد الشبكة المحدودة على تلك الصفحة بوصفها هيكلًا فارغًا، فقد تربط السلسلة صحيحة، لكن لم يُتحقق فعليًا شيء عن النص المحيط. أضف طبقة نص أولًا إن كان لذلك شأن. التذييلات المصونة صورًا لا نصًا غير مرئية لمنطق الشريطين وغير مؤذية للسبب نفسه

الشريطان رقم واحد. تذييل أعمق من ContinuationMargin يترك أسطره السفلى داخل منطقة المتني، فيبدو أن نصًا يتبع الشظية السابقة ويعطّل الربط؛ ارفع الخيار إلى عمق الشريط الحقيقي كما يفعل المثال الأول. وارفعه بعيدًا جدًا تسللت فقرة ختامية قصيرة قرب أسفل الصفحة إلى الشريط وتُغفل، فيرتبط جدول بما يليه مهما كان. ولقاعدة العنوان فشل مرآتي: مولد يكتب لافتة «continued» مدموجة أول صف من كل شظية إكمال ستُرفض تُلك الشظايا بوصفها جداول جديدة، والعلاج الوحيد اليوم أن تخيط بنفسك عبر ContinuationGroup دون أن تخفف شيئًا، لأن القاعدة بلا مفتاح

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

وسم الإكمال وقاعدة العنوان ومرور الصف الواحد كلها تقيم في مسار مستوى المستند المشترك بين بناءات دلفي وC++Builder وLazarus؛ وتوصف واجهة استخراج الجداول الكاملة على صفحة مكوّن PDFium لدلفي