يوقف مكوّن PDFium في الإصدار 3.117.0 الإبلاغ عن الفقرات المضبوطة بوصفها جداول محاذاة المسافات البيضاء باشتراط أن يكون كل حد عمود ممرًا رأسيًا خاليًا من النص على كل صف يفصله، مع تخطي الكلمات المطالبة سلفًا بشبكة محدودة، وتجميع نص الخلية بالتراكب الرأسي بدل مسافة مركز صندوق المحرف. التغييرات الثلاثة كلها تقيم داخل ExtractTables وExtractDocumentTables ولا تحتاج خيارًا
كان التقرير الذي أطلق هذا غير بهيح. صفحة بيان صحفي بلا أي جدول عليها عادت من ExtractTables بجدول مسافات بيضاء 5x4، بثقة مرتاحة فوق MinConfidence الافتراضية البالغة 0.5، وكانت الخلايا تحوي شذبًا من نص متني عادي. وفعل نموذج تسجيل الشيء نفسه بفقراته المقالية وأنتج 3x4 و5x3. كان كلا المستندين مضبوطًا الهوامش. الجواب البديهي هو ضبط العتبات، والدرس المفيد من هذا الإصدار أن الضبط لا يستطيع إصلاحه، لأن القاعدة التي كانت تُضبط كانت تسأل السؤال الخطأ
uses
PDFium;
// فحص انحدار: اسرد كل جدول مسافات بيضاء في مستند حتى يمكن التحقق من نظافة
// صفحة تعرف أنها نثر فقط
procedure ReportWhitespaceTables(Pdf: TPdf);
var
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Options := TPdfTableExtractionOptions.Default; // MinColumnGap 12pt
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
if Tables[I].DetectionMode = ptdmWhitespace then
Writeln(Format('page %d: %dx%d whitespace table, confidence %.2f, ' +
'first cell "%s"',
[Tables[I].PageNumber, Tables[I].RowCount, Tables[I].ColumnCount,
Tables[I].Confidence, Tables[I].Cells[0].Text]));
end;
لماذا يبدو النص المضبوط جدولًا؟
يبدو النص المضبوط جدولًا لأن السطر المضبوط صف من الكلمات تفصلها فجوات مدّها محرك التخطيط، وحالما تبلغ فجوة ممتدة MinColumnGap لا يملك الكاشف وسيلة على مستوى الصف ليميزها عن فاصل أعمدة. تجمع استراتيجية المسافات البيضاء في مكوّن PDFium صناديق الكلمات صفوفًا بصرية، وتشطر كل صف إلى مجموعات كلمات حيثما بلغت المسافة الأفقية إلى الكلمة السابقة MinColumnGap (12 نقطة افتراضيًا)، وتقبل جدولًا حين يكرر صفان متتاليان على الأقل MinColumns من مراسيس المجموعات المحاذاة يسارًا ضمن AlignmentTolerance وهي 3 نقاط. تلك هي القاعدة الموصوفة في نظرة كشف الجداول، وعلى جدول محاذى حقيقي هي صحيحة تمامًا
طبّقها الآن على عشرين سطرًا من نثر مضبوط بحجم 10 نقاط. كل سطر ممتد إلى الهامش الأيمن نفسه، فالسطر الذي ينتهي بكلمة طويلة يفتح مسافاته الداخلية، وفي فقرة فيها أسطر قليلة قصيرة تعبر بعض تلك المسافات 12 نقطة. يحتاج سطران متتاليان إلى فجوة ممتدة واحدة لكل منهما، تهبط ضمن 3 نقاط من موضع X نفسه، ليشكلا مرشحًا من صفين وعمودين. وعبر ما يكفي من الأسطر هذه ليست حظًا سيئًا؛ إنها احتمالية تقترب من اليقين، وكانت الـ 5x4 على البيان الصحفي ببساطة الجولة التي اصطف فيها أربع فجوات كهذه على خمسة أسطر
كل عتبة تداول صنفًا من المستندات ضد آخر. رفع MinColumnGap إلى 20 نقطة يضيع الأعمدة المضغوطة للتقارير المالية الكثيفة، وهي الحالة بالضبط التي خُفض الافتراض من أجلها أصلًا. ورفع MinRows إلى 3 يرمي الجداول الحقيقية ذات الصفين ويخفض الاحتمالات للفقرات الطويلة فقط. وشد AlignmentTolerance دون 3 نقاط يكسر صناديق كلمات OCR، التي ترتجف حوافها اليسرى أكثر من ذلك. إشارة مستوى الصف غامضة حقًا، فيجب أن يأتي الإصلاح من إشارة لا تحملها الصفوف من ذاتها
ما الذي يجعل حد عمود حقيقيًا؟
حد العمود الحقيقي شريط رأسي من الصفحة يبقى فارغًا عبر كل صف يفصله. الجدول يملك واحدًا بين كل زوج أعمدة بالبناء، لأن الخلايا وُضعت على مواضع X مشتركة. والفقرة المضبوط تمط مسافات كلماتها عند مواضع أفقية مختلفة في كل سطر، فلا ينجو شريط من تقاطع أكثر من سطر أو سطرين. يختبر مكوّن PDFium ذلك بالضبط الآن: بعد إسناد مجموعات كلمات المرشح إلى أعمدة المراسيس، لكل زوج أعمدة متجاورين تأخذ، في كل صف يحمل محتوى في الخليتين، الفترة من الحافة اليمنى لكلمات الخلية اليسرى إلى الحافة اليسرى لكلمات الخلية اليمنى، وتقاطع تلك الفترات عبر الصفوف، وترفض المرشح كله إذا كان التقاطع أضيق من MinColumnGap مضروبة في 0.5، أي 6 نقاط بالافتراض
تفصيلان يهمّان. الصفوف التي تكون إحدى خليتيها فارغة لا تصوت، فجدول فيه خلية فارغة، أو رأس يمتد على أعمدة أقل من المتن، ما يزال يجتاز. وعرض الممر يُشتق من MinColumnGap ولا يُكشف خيارًا منفصلًا، لأن الاثنين يصفان الشيء الفيزيائي نفسه: الفجوة التي يتركها المصمم بين الأعمدة. المنطق صغير بما يكفي لإعادة إنتاجه إن كنت تبني على صناديق كلمات خام لا على واجهة الجداول، وتعكس العينة أدناه الفحص داخل المكوّن:
uses
Math, PDFium;
type
TIndexList = array of Integer;
TCellIndexes = array of TIndexList; // Row * ColumnCount + Column
// تعيد False إذا انعدم لأي زوج أعمدة متجاور ممر عمودي خالٍ من النص
// بعرض MinColumnGap / 2 على الأقل عبر الصفوف التي تستخدمه
function HasTextFreeCorridors(const Words: TPdfWordBoxes;
const Cells: TCellIndexes; RowCount, ColumnCount: Integer;
MinColumnGap: Double): Boolean;
var
Col, Row, I, LeftCell, RightCell, Supported: Integer;
CorridorLeft, CorridorRight, RowLeft, RowRight: Double;
begin
for Col := 0 to ColumnCount - 2 do
begin
CorridorLeft := -MaxDouble;
CorridorRight := MaxDouble;
Supported := 0;
for Row := 0 to RowCount - 1 do
begin
LeftCell := Row * ColumnCount + Col;
RightCell := LeftCell + 1;
if (Length(Cells[LeftCell]) = 0) or (Length(Cells[RightCell]) = 0) then
Continue; // الخلايا الفارغة لا تصوت
RowLeft := -MaxDouble;
RowRight := MaxDouble;
for I in Cells[LeftCell] do
RowLeft := Max(RowLeft, Words[I].Rect.Right);
for I in Cells[RightCell] do
RowRight := Min(RowRight, Words[I].Rect.Left);
CorridorLeft := Max(CorridorLeft, RowLeft);
CorridorRight := Min(CorridorRight, RowRight);
Inc(Supported);
end;
if (Supported > 0) and
(CorridorRight - CorridorLeft < MinColumnGap * 0.5) then
Exit(False);
end;
Result := True;
end;
لماذا استُخرجت الجداول المحدودة مرتين؟
استُخرجت الجداول المحدودة مرتين لأن مرور المسافات البيضاء كان يرى كل كلمة في الصفحة، بما فيها الكلمات التي وضعها مرور الشبكات المحدودة سلفًا في شبكة، والجدول المحدود النظيف هو بالبناء أيضًا جدول مسافات بيضاء محاذى بإتقان. كان فحص تداخل يرفض سلفًا مرشح مسافات بيضاء تغطي حدودها أكثر من نصف جدول قائم، لكن مرشحًا جمع صفوف الجدول السفلى مع بضعة أسطر نص محاذاة تحته كان يمكن أن يهبط دون تلك النسبة وينجو جدولًا ثانيًا أكبر قليلًا ينزف إلى جاره. ينزع ExtractTables تلك الكلمات الآن قبل تشغيل مرور المسافات البيضاء. تُرمى كلمة حين يقع مركزها داخل حدود أي جدول أنتجه مرور الشبكات المحدودة؛ ويُستخدم المركز لا الاحتواء الكامل كي تتبع الكلمة العابرة لحد بكسر جزء من نقطة الجدول الذي تعود إليه بصريًا. وتشتغل استراتيجية المسافات البيضاء بعدها على الكلمات الحرة فقط، وهو ما يعني أيضًا أن جدولًا صغيرًا غير محدود يجثم مباشرة تحت محدود يُكشف بجدارته هو بدل الاندماج مع الشبكة فوقه
لماذا خرج «Purpose of Request:» بصيغة «of Purpose Request:»؟
خرجت الكلمات معاد ترتيبها لأن صناديق الكلمات التي يبنيها مكوّن PDFium اتحادات لصناديق إحاطة المحارف، و«of» لا ذات نازل بينما «Purpose» و«Request:» لهما. يعيد FPDFText_GetCharBox الصندوق المحكم لحبر المحرف في فضاء الصفحة، لا صندوقًا مبطنًا إلى صعود الخط ونزوله، وصندوق الكلمة اتحاد صناديق محارفها. الكلمة بلا نوازل إذن أقصر ومركزها الرأسي أعلى، بـ 2 إلى 3 نقاط على النموذج المعني. كان روتين نص الخلية القديم يرتب الكلمات بالمركز Y أولًا، بسماحية 1 نقطة لـ«السطر نفسه»، ثم بالحافة اليسرى؛ تجاوزت «of» السماحية، فارتبت سطرًا مستقلًا فوق البقية، وصدرت أولًا
هذه ليست طباع PDFium بقدر ما هي نتيجة كيفية تموضع PDF للنص. يعرف ISO 32000-1 §9.2.2 و§9.4.4 تموضع المحرف بوصفه إزاحة أفقية على خط الأساس في فضاء النص، والمقاييس الرأسية الوحيدة التي يحملها الملف لكل خط: مدخلات Ascent وDescent وFontBBox في واصف الخط في §9.8.1. لا شيء في الملف يقول إن محرفين يتشاركان سطرًا؛ ذلك يستدل من الهندسة، والصناديق المحكمة للمحارف التي تجعل إبراز التحديد يبدو صحيحًا، كما في اختيار أسطر النص بصناديق محارف PDFium، هي المدخل الخطأ لمقارنة مسافة مركز
يغيّر الإصلاح في الإصدار 3.117.0 السؤال من «ما بعد المركزين» إلى «كم يتداخل الصندوقان رأسيًا». يجمع نص الخلية بتجميع كلمات الخلية أولًا أسطرًا بصرية، حيث تنضم كلمة إلى سطر حين يكون تراكبها الرأسي مع حدود السطر الجارية 25 بالمئة على الأقل من أصغر الارتفاعين، ثم يرتب كل سطر بالحافة اليسرى فرزًا بالإدراج، ثم يلحق الأسطر بفاصل سطر. تتداخل «Purpose» و«of» فوق ارتفاع x كاملًا، وهو ما يفوق 25 بالمئة من الصندوق الأقصر بكثير، فتهبطان على السطر نفسه وترتبان بـ X كما يجب
جمع أسطر النص بالتراكب لا بمسافة المركز
القاعدة المستحقة للأخذ من هذا الخطأ عامة: أي كود تخطيط نص PDF يقرر «السطر نفسه» بمقارنة المراكز الرأسية بسماحية ثابتة سيفشل على الخطوط الحقيقية، والفشل صامت: لا شيء يرفع خطأ، والكلمات تخرج ببساطة بترتيب خاطئ. النوازل المختلطة أخف المحفزات. عنوان عريض بحجم 12 نقطة بجوار قيم بحجم 10، وعلامة حاشية عليا، ورمز عملة مرسوم من خط احتياطي، وصناديق كلمات OCR بضجيج ارتفاع لكل كلمة، كلها تحرك المراكز أكثر من أي سماحية ما تزال تفصل أسطرًا متجاورة من نص 10 نقاط عند قياد 12 نقطة. نسبة التراكب مستقلة عن الحجم: صندوقان على خط أساس واحد يتداخلان فوق ارتفاع x المشترك مهما فعلت صواعدهما ونوازله، وصندوقان على سطرين متجاورين لا يتداخلان بشيء
والقاعدة نفسها سهلة التطبيق خارج استخراج الجداول. يعيد TPdf.PageWordBoxes كل كلمة على الصفحة النشطة بمستطيلها في فضاء الصفحة، فتجميع صفحة أسطرًا بصرية حلقة قصيرة:
uses
Math, PDFium;
function SameVisualLine(const A, B: TPdfRectangle): Boolean;
var
Overlap, MinHeight: Double;
begin
Overlap := Min(A.Top, B.Top) - Max(A.Bottom, B.Bottom);
MinHeight := Min(A.Top - A.Bottom, B.Top - B.Bottom);
Result := (MinHeight > 0) and (Overlap >= MinHeight * 0.25);
end;
procedure GroupPageIntoLines(Pdf: TPdf; out Lines: TArray<TPdfWordBoxes>);
var
Words: TPdfWordBoxes;
Bounds: TArray<TPdfRectangle>; // اتحاد جارٍ لكل سطر
I, J, Found: Integer;
begin
Words := Pdf.PageWordBoxes;
Lines := nil;
Bounds := nil;
for I := 0 to High(Words) do
begin
Found := -1;
for J := High(Lines) downto 0 do
if SameVisualLine(Bounds[J], Words[I].Rect) then
begin
Found := J;
Break;
end;
if Found < 0 then
begin
SetLength(Lines, Length(Lines) + 1);
SetLength(Bounds, Length(Bounds) + 1);
Found := High(Lines);
Bounds[Found] := Words[I].Rect;
end;
SetLength(Lines[Found], Length(Lines[Found]) + 1);
Lines[Found][High(Lines[Found])] := Words[I];
Bounds[Found].Left := Min(Bounds[Found].Left, Words[I].Rect.Left);
Bounds[Found].Right := Max(Bounds[Found].Right, Words[I].Rect.Right);
Bounds[Found].Top := Max(Bounds[Found].Top, Words[I].Rect.Top);
Bounds[Found].Bottom := Min(Bounds[Found].Bottom, Words[I].Rect.Bottom);
end;
// رتب كل سطر بـ Rect.Left قبل قراءته؛ يعيد PageWordBoxes
// الكلمات بترتيب تدفق المحتوى، وهو غير مضمون أن يكون بصريًا
end;
ما الذي يتغير للمدققين القائمين، وأين الحدود
مغزى تلك الشفرة هو المسند لا الحلقة؛ ولفوق أي تفريغ سريع، ابدأ من نموذج النص المهيكل الذي يحمل سلفًا كتلًا وأسطرًا ومصدر ترتيب قراءة، كما في استخراج نص PDF المهيكل بترتيب القراءة. ينال مدققو الجداول القائمون التصحيحات الثلاث كلها دون لمس خياراتهم. عتبة الممر ثابتة عند نصف MinColumnGap، وتحفظ استراتيجية المسافات البيضاء أرضيتها ذات الصفين حتى حين تضبط MinRows على 1 (والتي يقبلها الآن مرور الشبكات المحدودة)، وتصفية الكلمات بالشبكات أولًا غير مشروطة كلما فعال الاستراتيجيتان. على مجموعة العينات ذات 13 مستندًا المستخدمة للإصدار، كان مرور المسافات البيضاء قد أعاد سلفًا 34 شظية وإيجابية كاذبة إلى جانب 9 جداول محدودة؛ وبعد الإصدار لا يعيد شيئًا منها، وارتفع عدد الجداول المحدودة إلى 41، وإن كان معظم تلك الزيادة يأتي من الإصدار نفسه الذي علّم كاشف الشبكات قراءة الحدود المرسومة مستطيلات معبأة، وهي قصة أخرى
الحدود الصادقة: يحتاج اختبار الممر صفًا واحدًا على الأقل فيه محتوى على جانبي حد ليرفض شيئًا، فمرشح من صفين تصادف أن فجوتيه الممطوطتين هبطتا ضمن 6 نقاط من بعضهما ما يزال يجتاز. تلك مصادفة ضيقة لا يقين سابق، لكن المستندات الغارقة في النثر بلا جداول حقيقية ذات صفين تستطيع إغلاقه بضبط MinRows على 3. النص المحاذى يسارًا المتدلي لم يكن المشكلة قط ولا يتأثر. وما يزال PDF بلا كائن جدول؛ يعرف ISO 32000-1 §14.8.4.3 عنصر بنية Table، لكن PDF الموسوم فقط يحمله، فلكل ما سواه تبقى الشبكة استدلالًا من الهندسة، وقيمة الثقة على كل TPdfTable موجودة لأن الاستدلال يستحق درجة
يقرأ استخراج الجداول والنص المهيكل وصناديق الكلمات كلها من نموذج الصفحة نفسه في دلفي وC++Builder وLazarus؛ وتوصف الواجهة الكاملة، بما فيها TPdfTableExtractionOptions والعرض التوضيحي TableExtractionLab الذي يأتي معها، على صفحة مكوّن PDFium لدلفي