مقال تقني

حدود جداول من مستطيلات معبأة كخطوط حصر في PDFium لدلفي

يعامل استخراج الجداول في مكوّن PDFium، منذ الإصدار 3.117.0، المستطيل المعبأ الرفيع بوصفه خط حصر للجدول. مع تفعيل DetectFilledRulings، وهو الافتراضي، يصير الصندوق المعبأ المحاذي للمحاور الأسمك من MaxRulingThickness (3 نقاط) خط حصر واحدًا على محوره الطويل، ويسهم الصندوق المعبأ الأكبر بحوافه الأربع، وتُلائم كل إحداثية خط حصر ضمن RulingSnapTolerance (4 نقاط) قبل تركيب الشبكة. لذلك تصل الجداول المصدَّرة من Word وGoogle Docs والمتصفحات إلى كاشف الشبكات المحدودة شبكات مكتملة بدل أن تسقط إلى كشف المسافات البيضاء شظايا

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

لماذا لا يحمل جدول مصدَّر من Word خطوطًا مشطوبة؟

لا يفكر معالج النصوص في حد بوصفه خطًا؛ بل يفكر فيه بوصفه صندوقًا بعرض، ويرسم ذلك الصندوق بتعبئة. يعرف ISO 32000-1 §8.5.2.1 المعامل re بوصفه إلحاق مسار فرعي مستطيل، ويفصل §8.5.3 معاملات الرسم: S يشطب المسار بعرض الخط الحالي، وf يملأ بنيته. حد خلية بعرض 0.5 نقطة يخرج بالصيغة x y w 0.5 re f، ولا يعمل ميكانيزم الشطب قط، بعرض الخط ووصلاته ونمط الشرطات. وتعتيم الخلية هو البناء نفسه بصندوق أكبر. الشبكة المشطوبة المرسومة بـ m وl وS هي ما توقعه الكاشف الأصلي، وهي ما لا ينتجه تقريبًا أي شيء يُصدَّر من تطبيق أوفيس:

% حد خلية من تصدير معالج نصوص: صندوق معبأ بارتفاع 0.5 pt
72 700 468 0.5 re f
% تعتيم خلية: صندوق معبأ بحجم الخلية
72 676 117 24 re f
% سطر الشبكة المشطوب الذي كُتب الكاشف الأصلي لأجله
72 700 m 540 700 l S

لن كاشف يسأل FPDFPath_GetDrawMode عن كون علم الشطب مضبوطًا فحسب، كلا الصندوقين المعبأين غير مرئي. ثم تبلغ الكلمات داخل الخلايا كشف المسافات البيضاء، حيث تقع أعمدة يفصلها حاضن بعرض 6 نقاط دون MinColumnGap الافتراضي البالغ 12 نقطة، وما يعود هو أي subset من الصفوف صادف أن حاذى جيدًا بما يكفي لاجتياز MinRows. تلك هي سلوكية الشظايا، ولا قدر من ضبط المعاملات يحولها إلى الشبكة التي رسمها المؤلف

كيف يحوّل مكوّن PDFium صندوقًا معبأً إلى خط حصر؟

يفحص TableCollectObjectRulings كل كائن مسار مسارًا فرعيًا في كل مرة. يأتي وضع الرسم من FPDFPath_GetDrawMode؛ ويُعد المسار معبأً حين تكون DetectFilledRulings مفعلة ووضع التعبئة ليس none. تُحوَّل كل نقطة عبر مصفوفة الكائن وتُجمع، حتى MaxSubpathPoints (8) لكل مسار فرعي، ويمسّ أي مقطع منحني المسار الفرعي بوصفه منحنيًا. حين يُغلق المسار الفرعي أو يبدأ MoveTo جديد يقرر FlushSubpath ما كان: المسار الفرعي المنحني يُرمى، وكذلك أي مضلع مغلق لا تجثم نقاطه كلها ضمن PointTolerance (0.05 نقطة) من حواف الصندوق المحيط على محور واحد على الأقل. مثلث أو شيفرون أو تبويب دائري لا يصير خط حصر قط، وهذا ما يبقي الرسوم الزخرفية خارج الشبكة

مخطط مكوّن PDFium لكيفية تحويل TableCollectObjectRulings المسارات الفرعية المغلقة إلى خطوط حصر للجداول في دلفي: يرمي FlushSubpath الحدود المنحنية والمضلعات البعيدة عن حواف الصندوق المحيط، ويشطر MaxRulingThickness الصناديق الرفيعة إلى خط حصر لكل محور طويل، وتعطي الخلايا المعتمة أربعة خطوط حواف ويبقي DetectFilledRulings المربعات الضئيلة خارجًا
لا ينجو مسار فرعي مغلق إلا حين يكون محاذيًا للمحاور، وعندها يقرر الصندوق المحيط هل هو خط حصر واحد أم أربع حواف لخلية معتمة أم لا شيء إطلاقًا

ما ينجو مستطيل محاذٍ للمحاور، يُصنَّف بصندوقه المحيط. عرض عند MaxRulingThickness أو دونه مع ارتفاع فوقه يعطي خط حصر عموديًا واحدًا عند المركز الأفقي يمتد بالصندوق من أسفل إلى أعلى؛ والحالة المرآوية تعطي خط حصر أفقيًا واحدًا. كلا البعدين فوق العتبة يعني خلية معتمة، ويسهم الصندوق بأربعة خطوط، خطًا لكل حافة. وكلا البعدين عند العتبة أو دونها لا يسهم بشيء، فرصاصة مربعة بحجم نقطتين لا تُخطأ خطًا. المسار المشطوب يسلك الطريق الأقدم عبر AddLine، خط حصر لكل مقطع محاذٍ للمحاور، فتُعالج الشبكة المرسومة بـ S تمامًا كما قبل، والمسار المرسوم بتعبئة وشطب معًا ينتج قطعًا متداخلة تنهار في مرور الدمج:

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  Mode: string;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'itinerary-from-word.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;                     // يبدأ من 1

    Options := TPdfTableExtractionOptions.Default;
    // هذه افتراضيات 3.117.0، مفصّلة للتوضيح
    Options.DetectFilledRulings := True;     // الصناديق المعبأة الرفيعة تصير خطوط حصر
    Options.MaxRulingThickness := 3.0;       // نقاط؛ الصناديق الأسمك تُعد تعتيمًا
    Options.RulingSnapTolerance := 4.0;      // نقاط؛ الصفر يعطّل المحاذاة
    Options.IncludeFormXObjects := True;

    Tables := Pdf.ExtractTables(Options);
    for I := 0 to High(Tables) do
    begin
      if Tables[I].DetectionMode = ptdmRuled then
        Mode := 'ruled'
      else
        Mode := 'whitespace';
      Writeln(Format('%dx%d %s, confidence %.2f',
        [Tables[I].RowCount, Tables[I].ColumnCount, Mode,
         Tables[I].Confidence]));
    end;
  finally
    Pdf.Free;
  end;
end;

ماذا يفعل RulingSnapTolerance لجداول الخلايا المعتمة؟

RulingSnapTolerance هو ما يجعل جدولًا مبنيًا من التعتيم وحده يتصل شبكةً واحدة. بعض الصادرات لا ترسم حدًا إطلاقًا: كل خلية صندوق معبأ بلونه الخاص، والصناديق المتجاورة تفصلها حاضنة بيضاء بعرض 1 إلى 3 نقاط. يعطي كل صندوق أربعة خطوط حواف، لكن الحافة اليمنى لخلية والحافة اليسرى للتي تليها تجثمان على بعد نقطتين، ويستخدم اختبار الاتصال RulingTolerance الافتراضية 1 نقطة. بلا محاذاة تشكل كل خلية مكونها المتصل من أربعة خطوط، ولا يبلغ أي مكون MinRows، ولا تبلغ الصفحة عن شيء. يجمع TableSnapRulings كل إحداثية X في اللعب (موضع كل خط حصر عمودي زائد بداية ونهاية كل أفقي) وكل إحداثية Y بالمثل، ويرتب كل قائمة، ويُعنقدها بتسلسل القيم التي لا يزيد اختلاف جارها عن السماحية، ويستبدل كل عنقود بمتوسطه، ثم ينقل كل موضع وبداية ونهاية إلى أقرب مركز عنقود. فيصير جانبا الحاضنة الخط نفسه، ويثبت الاتصال

مخطط مكوّن PDFium لربط RulingSnapTolerance جدول خلايا معتمة في دلفي: تترك الخلايا المتجاورة حاضنة 2 pt وتجثم خطوط حوافها خارج RulingTolerance البالغة 1 pt، ويسلسل TableSnapRulings قيمتي X في متوسط عنقود واحد فيرى اختبار الاتصال أخيرًا سطر شبكة مشتركًا
تشترك المحاذاة قبل الدمج وقبل كاشف الشبكات المحدودة، فيصير جانبا الحاضنة البيضاء خطًا واحدًا وتتوقف كل خلية عن كونها جزيرة من أربعة خطوط

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

// اعزل استراتيجية الشبكات وقارن ما يرى كل ضبط على صفحة واحدة
function CountRuledTables(Pdf: TPdf; FilledRulings: Boolean;
  SnapTolerance: Double): Integer;
var
  Options: TPdfTableExtractionOptions;
begin
  Options := TPdfTableExtractionOptions.Default;
  Options.DetectWhitespaceTables := False;
  Options.DetectFilledRulings := FilledRulings;
  Options.RulingSnapTolerance := SnapTolerance;
  Result := Length(Pdf.ExtractTables(Options));
end;

// تصدير Word يبلّغ عادة 0 ثم N ثم أقل من N:
// الشطب وحده لا يرى شيئًا، والمحاذاة تصل خلايا التعتيم،
// وتعطيل المحاذاة يترك كل خلية معتمة جزيرتها الخاصة
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

خطوط حصر داخل form XObjects

تلتف أدوات تخطيط الصفحات تكرارًا بجدول، أو بجسم الصفحة كله، في form XObject وترسمه بـ Do. يحدد ISO 32000-1 §8.10.1 أن مصفوفة الاستمارة تُدمج مع مصفوفة التحويل الحالية عند رسم الاستمارة، فالمستطيل داخل الاستمارة يقيم في فضاء الاستمارة ولا يهبط على الصفحة إلا بعد تحويلين أو أكثر. يتفرع TableCollectObjectRulings إلى كائنات الاستمارة حين تضبط IncludeFormXObjects: يقرأ مصفوفة الكائن، ويجمعها مع مصفوفة الأب عبر TableMultiplyMatrix التي يعني ترتيب معاملاتها «مرّ عبر المصفوفة الأولى ثم الثانية»، ويسرد الأولاد بـ FPDFFormObj_CountObjects وFPDFFormObj_GetObject مارًّا المصفوفة المجمعة نزولًا. التعشيش الأعمق من MaxFormDepth (8) يُتخطى بصمت، وهو حارس ضد ملفات مرضية لا حد يقترب منه أي تصدير حقيقي. وسبب أهمية ترتيب الضرب هو نفسه الذي نوقش في الإلحاق الأمامي مقابل الخلفي للمصفوفة: تبديل المعاملين ينقل حد الترجمة، وخط حصر يفترض أن يهبط أعلى الصفحة يهبط عند نقطة الأصل بدلًا من ذلك

مخطط مكوّن PDFium لخطوط الحصر داخل form XObject في دلفي: مستطيل رفيع مؤلَّف بصيغة 72 700 468 0.5 re f يقيم في فضاء الاستمارة ولا يهبط على الصفحة إلا بعد أن تجمع TableMultiplyMatrix مصفوفة CTM الأب مع مصفوفة الاستمارة، متفرعةً عبر FPDFFormObj_CountObjects حتى MaxFormDepth
يؤلَّف المستطيل في فضاء الاستمارة ولا يبلغ أعلى الصفحة إلا بعد ضرب المصفوفات بترتيب يحفظ حد الترجمة حيث يجب أن يكون

لماذا تضاعفت ميزانية خطوط الحصر أربع مرات؟

ارتفع الافتراض MaxRulingSegments من 4096 إلى 16384 في 3.117.0 لأن حدود الخلايا لكل خلية تصل بأعداد تفوق خطوط الشبكة المشطوبة بكثير. جدول مشطوب بـ 30 صفًا و6 أعمدة هو 38 مقطع خط. الجدول نفسه المصدَّر صناديق معبأة يصل إلى أربعة حدود لكل خلية، أي 720 قطعة قبل الدمج، واستمارة بخلايا معتمة تضاعف ذلك. جدولان كهذاين على صفحة كانا سينضبان الميزانية القديمة. تُفرض الميزانية في TableAppendRuling عبر Check الذي يرفع EPdfError برسالة «Table ruling-segment budget exceeded»؛ لا نتيجة متدهورة ولا شبكة جزئية، ولا يشتغل مرور المسافات البيضاء أيضًا. وإن ضبطت ميزانية أضيق من عندك لمدخلات غير موثوقة، فالتقط الاستثناء وقرر، ولا تقرأ نتيجة فارغة بمعنى «لا جداول»:

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // ضيق عمدًا لمدخلات غير موثوقة
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // الافتراضي في 3.117.0
    Tables := Pdf.ExtractTables(Options);
  end;
end;

نتائج مقيسة وأين تتوقف المقاربة

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

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