يستعيد HotPDF الجداول من PDF موجود عبر ExtractLoadedTypedTables، وهي API لـDelphi تدمج أجزاء الصفوف التي تنتجها تمريرة التخطيط، وتبني شبكة أعمدة معيارية واحدة لكل جدول، وتواصل الجدول عبر فاصل الصفحة عندما تدعم الهندسة ذلك، وتعيد كل خلية كقيمة مهيكلة تحمل مصدر الصفحة وامتداد العمود والحدود. وتكتب ExportLoadedTypedTables النتيجة نفسها مباشرة إلى CSV أو JSON. والسيناريو الذي يجعل بناء هذا مفيدًا ممل ومألوف للغاية. سجل فواتير من أربعين صفحة، وهو جدول واحد منطقيًا، طُبع مع تكرار رأسه أعلى كل صفحة. شغّل عليه تمريرة ترتيب قراءة ساذجة، فتحصل على أربعين جدولًا، وتسعةً وثلاثين صف رأس زائفًا، وعمود عملة ينزلق موضعًا واحدًا إلى اليسار في كل صف صادف أن خليته الوسطى كانت فارغة. وتنظيف ذلك لاحقًا داخل التطبيق المستدعي هو الموضع الذي تموت فيه مشاريع استيراد المستندات
لماذا تسلمك صفحة PDF أجزاء بدل جدول؟
لأن صفحة PDF لا تحمل دلالات جداول أصلًا ما لم يكن المستند موسومًا. يحمل تدفق المحتوى معاملات إظهار النص ومصفوفات التموضع (ISO 32000-1 §9.4.3) ولا شيء أكثر؛ فالصندوق ذو الخطوط الذي تراه على الشاشة هو رسم مسار منفصل لا يُلزم أي مستخرج بربطه بالنص. وتعيش أنواع عناصر البنية Table وTR وTH وTD فقط في تسلسل البنية المنطقي لـPDF موسوم (ISO 32000-1 §14.8.4)، بينما الغالبية الساحقة من مستندات الأعمال المتداولة غير موسومة. وكل ما نذكره أدناه استعادة هندسية لا تحليلًا دلاليًا، ومن المفيد قول ذلك بصوت واضح قبل أن يبني أحد تقرير مطابقة فوقه
لذلك يجري HotPDF أولًا تحليل تخطيط دلاليًا فوق glyphs المستخرجة، وهو المسار نفسه الذي يدعم استخراج النص بترتيب البنية من PDF محمل وتصديرات HTML وXML المهيكلة. تجمع هذه التمريرة خطوط الأساس في مسارات تصطف خلاياها رأسيًا، ولا تواصل المسار إلا ما دامت الصفوف المتتالية تملك عدد الخلايا نفسه. وبالنسبة إلى محرك تخطيط، هذه القاعدة صحيحة ورخيصة. أما للمستدعي فهي الشكل الخطأ: فصف واحد يحوي خلية داخلية فارغة يقسم جدولًا مرئيًا واحدًا إلى جدولي مصدر. وتوجد طبقة الجدول المهيكل فوق تلك التمريرة تحديدًا لإعادة القطع إلى مكانها
شبكات الأعمدة المعيارية ومفتاح ColumnTolerance
يدمج ExtractLoadedTypedTables الأجزاء الموجودة في الصفحة نفسها قبل أن يفعل أي شيء آخر، ويدمجها بناءً على هندسة الأعمدة لا نص الصف. ينضم جدولان مصدران متجاوران في صفحة واحدة عندما يملك كل منهما عمودين على الأقل، وعندما تبقى الفجوة الرأسية بين آخر صف من الأول وأول صف من الثاني داخل نطاق السماحية، وعندما تصطف مواضع بداية أعمدتهما. وتنهار بدايات الأعمدة التي تفصلها قيمة ضمن ColumnTolerance إلى عمود معياري واحد، ثم تُحسب متوسطاتها أثناء الدمج. وتبلغ السماحية الافتراضية 12 وحدة من مساحة المستخدم، وهي تناسب الطباعة العادية في مستندات الأعمال، وتحتاج إلى رفعها مع التخطيطات ذات التباعد الواسع أو البادئات العميقة
لكن ما يحدث للصف الذي يفتقد قيمة داخلية هو الجزء المهم. يثبت HotPDF كل خلية عند أقرب بداية عمود معيارية، ثم يضبط ColumnSpan على المسافة من ذلك العمود إلى العمود المشغول التالي، بدل إزاحة الخلايا المتبقية إلى اليسار. فيحافظ صف من ثلاث خلايا داخل شبكة من خمسة أعمدة على قيمه تحت العناوين الصحيحة، ويسجل بدقة مواضع الفجوات. وهذا هو الفرق بين جدول تستطيع مطابقته وجدول ينسب المال بصمت إلى المكان الخطأ
var
Pdf: THotPDF;
Options: THPDFTypedTableExtractionOptions;
Tables: THPDFTypedTables;
Info: THPDFTypedTableExtractionInfo;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('register.pdf', '') <= 0 then
Exit;
Options := THPDFTypedTableExtractionOptions.Default;
Options.ColumnTolerance := 12; // وحدات مساحة المستخدم
Options.MinimumTableConfidence := 0.55; // تُسقط الجداول تحت هذه القيمة
Options.DateOrder := ttdoDMY; // 03/04/2026 يعني 3 أبريل
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// يوضح Info.TableCount مقابل Info.SourceTableCount مقدار ما دُمج
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
ما الذي يضمنه الدمج عبر الصفحات فعلًا؟
يضمن التحفظ، عن قصد. إذ يضم HotPDF جدولين عبر حد الصفحة فقط عندما يكون MergeAcrossPages فعالًا، وعندما يبدأ الجدول الثاني عند فهرس الصفحة الذي يلي بالضبط الصفحة التي انتهى عندها الأول، وعندما يملك كلاهما عمودين على الأقل، وعندما تصطف بدايتا عمود معياريتان على الأقل داخل ColumnTolerance. وشرط الصفحات المتتالية هو الذي يحمل العبء الأكبر. إذ يمرر المستدعون PageIndices كمصفوفة مفتوحة بأي ترتيب يريدونه، ومن دون هذا الفحص قد يلحم طلب للصفحات 3 و9 و14 ثلاثة جداول لا علاقة بينها في نتيجة واحدة تبدو معقولة تمامًا. والكلفة هي أن استمرارًا حقيقيًا يتخطى صفحة، أو ملحقًا متداخلًا، أو مسحًا على الوجهين له ظهر فارغ، يعود جدولين، ولا يخفف أي خيار هذا الشرط. وإعادة ضم تلك الحالات قرار سياسة لا يستطيع اتخاذه إلا التطبيق المستدعي، ولذلك تكشف API FirstPageIndex وLastPageIndex وSourceTableCount وPageIndex لكل صف، وتترك القرار في موضعه الصحيح
تُوسم الرؤوس المتكررة ولا تُحذف أبدًا
لا يزيل ExtractLoadedTypedTables صف رأس متكررًا من النتيجة مطلقًا. فعندما يجد الدمج عبر الصفحات أن الجدول الوارد يبدأ بنص رأس مطابق للجدول المتراكم، بعد إزالة المسافات وتوحيد حالة الأحرف، يضع على تلك الصفوف العلمين IsHeader وIsRepeatedHeader، ثم يضيفها بترتيب المصدر على أي حال. فالحذف اختيار فاقد وغير قابل للعكس، ويريد المستهلكون إجابات مختلفة: استيراد CSV يريد إزالة التكرارات، وسجل التدقيق يريدها مع أرقام صفحاتها، وأداة المقارنة تريد الحفاظ على ترتيب المصدر بايتًا ببايت. ولذلك تبلغ المكتبة، ويقرر المستدعي
var
T, R, C: Integer;
Row: THPDFTypedTableRow;
Total: Double;
begin
Total := 0;
for T := 0 to High(Tables) do
for R := 0 to High(Tables[T].Rows) do
begin
Row := Tables[T].Rows[R];
if Row.IsRepeatedHeader then
Continue; // أبقِ كتلة الرأس الأولى فقط
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
القيم المهيكلة والفواصل التي يجب أن توفرها
يعمل استنتاج النوع بترتيب ثابت يحل الالتباسات في الاتجاه الوحيد المعقول: boolean أولًا، ثم date، ثم percentage، ثم currency، ثم number عادي، وما لا يطابق يبقى string. والترتيب هو ما يمنع 2026 في عمود تاريخ من أن يقرره محلل الأرقام قبل أن يراه محلل التاريخ. وتُعرف العملة من $ أو £ أو ¥ أو € في المقدمة، أو من رمز ISO 4217 من ثلاثة أحرف يتبعه فراغ، ويُحفظ الرمز في CurrencyCode. والأهم أن HotPDF لا يخمن locale الخاص بك. فالقيم DecimalSeparator وThousandsSeparator وDateOrder تأتي من الخيارات، لأن 1.234 قد يكون رقمًا واحدًا أو ألفًا ومئتين وأربعة وثلاثين بحسب حقيقة لا يحتويها PDF. ويُحتفظ بنص Unicode الخام Text في كل خلية إلى جانب القيمة المهيكلة، ولذلك يمكن استعادة التخمين الخاطئ دائمًا من دون تمريرة استخراج ثانية
var
Stream: TFileStream;
Info: THPDFTypedTableExtractionInfo;
begin
Stream := TFileStream.Create('tables.json', fmCreate);
try
if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
Stream, Options, Info) then
case Info.Status of
ttesInvalidOptions: ReportBadConfiguration;
ttesBudgetExceeded: ReportOversizedDocument;
ttesCancelled: ReportUserCancelled;
ttesWriteFailed: ReportDestinationProblem;
else
ReportExtractionFailure;
end;
finally
Stream.Free;
end;
end;
يجيب تنسيقا التصدير عن سؤالين مختلفين، وهما غير متكافئين عمدًا. يكتب CSV أعمدة الاستمرار في الامتداد المدمج كحقول فارغة، وهو ما تتوقعه ورقة جدول أو أداة تحميل جماعي. أما JSON فيحتفظ بكل ما عرفه الاستخراج: القيمة المهيكلة تحت نوعها، وcolumnSpan، والثقة لكل خلية ولكل صف، وحدود الخلية، ومصدر الصفحة والجدول. ويضع كلا التنسيقين المستند كاملًا في مخزن ذاكرة محدود ثم ينشره إلى تدفق الوجهة، ويعيد البايتات الأصلية والطول والموضع إذا فشلت الكتابة في منتصفها، ولذلك لا يترك التصدير الفاشل ملفًا مكتوبًا نصفه. وتُحسب ميزانيات الصفحات وglyphs لكل صفحة والجداول والصفوف والخلايا والمحارف وبايتات الإخراج منفصلة كلها، كما تُعد الصفوف قبل التخصيص لأن SetLength لكل صف يتحول إلى نسخ تربيعي قبل الوصول بزمن طويل إلى سقف المليون صف الافتراضي
أين تتوقف استعادة الجدول الهندسية؟
إن التصريح بأوضاع الفشل أكثر فائدة من قائمة الميزات، لأن كل نقطة من هذه تحتاج إلى سياسة خاصة بالمستدعي لا إلى قيمة خيار أفضل
- لا تُستعاد الدمجات الرأسية. يبلغ HotPDF عن
ColumnSpanللامتدادات الأفقية ويتركRowSpanعند 1، ولذلك تصل الخلية التي تمتد عبر ثلاثة صفوف في الجدول المطبوع كخلية واحدة وفجوتين - اكتشاف الرأس معتمد على البيانات لا على الشكل. فكتلة الرأس هي مسار الصفوف السابق لأول صف يحتوي على قيمة مهيكلة غير string، ولذلك يبلغ جدول جسمه كله نصًا عن
HeaderRowCountبقيمة صفر مهما كان تنسيقه - تُسقط الجداول التي تقل عن
MinimumTableConfidenceمن النتيجة من دون خطأ. قارنInfo.TableCountبـInfo.SourceTableCountعندما تحتاج معرفة أن شيئًا قد أُسقط - يحتاج المسار إلى صفين على الأقل وعمودين على الأقل قبل أن تعتبره تمريرة التخطيط جدولًا أصلًا، ولذلك لا يكون الجدول الوهمي ذي السطر الواحد أو التخطيط ذو العمودين للنثر الطويل جدولًا، بصورة صحيحة وغير مفيدة
- لا تحتوي الصفحات الممسوحة على معاملات نص، ولذلك لا يوجد شيء يمكن استعادته هندسيًا حتى توجد طبقة نص OCR على الصفحة
إذا كانت ملفات PDF لديك تخرج من مكدس تقاريرك الخاص، فأرخص إصلاح لكل ذلك هو المنبع: أصدر جداول موسومة، أو احتفظ ببيانات المصدر، وتعامل مع الاستخراج كرجوع احتياطي للمستندات التي لم تنتجها. أما في غير ذلك، فتستحق دراسة المسار بهذا الترتيب لأن كل طبقة تبني على التي تحتها: ابدأ بـاستخراج النص العادي من PDF محمل، وانتقل إلى API الجداول المهيكلة عندما يجب الحفاظ على الهندسة، وانظر إلى تصيير جدول بيانات إلى PDF جديد عندما تكون في جانب التوليد وتستطيع تقرير مدى قابلية استعادة الناتج
تأتي ExtractLoadedTypedTables وExportLoadedTypedTables ضمن مكوّن HotPDF PDF الأصلي لـDelphi لـDelphi وC++Builder، من دون DLL خارجية أو اعتماد وقت تشغيل؛ وتضم صفحة المنتج المرجع الكامل للخيارات والحالات والسجلات الخاصة بـAPI الجداول المهيكلة