کامپوننت PDFium نسخهٔ 3.117.0 جدولی را که روی مرز یک صفحه میشکند وقتی به هم میبندد که یا هر دو قطعه لبههای صفحه را لمس کنند یا هیچ متن اصلی زیر قطعهٔ اول و بالای قطعهٔ دوم نباشد، با نادیده گرفتن سرصفحه و پاصفحهٔ جاری. ExtractDocumentTables این آزمون محتوامحور را بهعنوان جایگزینی برای آزمون قدیمیتر حاشیهٔ صفحه اعمال میکند، قطعهای در صفحهٔ بعدی که سطر اولش یک سلول caption تمام-عرض تکی است را رد میکند، و یک سطر تکی که به صفحهٔ بعدی سرریز شده را بهعنوان بخشی از زنجیرهٔ ادامهاش نگه میدارد
مقالهٔ تشخیص و استخراج جدول ادامه را بهصورت چهار gate سختگیرانه معرفی کرده و «لمس لبهٔ صفحه» را یکی از آنها شمرده بود. آن توصیف برای انتشار زمان خودش درست بود، و برای بیشتر جدولهایی که مردم واقعاً به کامپوننت میدهند هم غلط بود. این مقاله تصحیح است: آزمون حاشیه کدام سندها را نمیتواند اداره کند، چه چیزی جایش را گرفت، و آن دو حالت جانبی که fix با خودش کشید
چرا آزمون حاشیهٔ صفحه روی exportهای Word شکست میخورد؟
آزمون حاشیهٔ صفحه شکست میخورد چون یک واژهپرداز چیدن سطرها را در حاشیهٔ پایین متوقف میکند، نه در لبهٔ کاغذ. با مقدار پیشفرض ContinuationMargin برابر 36 پوینت، قاعدهٔ اصلی میخواست لبهٔ پایینی قطعهٔ قبلی در فاصلهٔ 36 پوینتی ته صفحه بیفتد و لبهٔ بالایی قطعهٔ بعدی در فاصلهٔ 36 پوینتی بالای صفحه. سندی که از Word با حاشیههای یک-اینچی پیشفرضش export شده، آخرین سطرش را حداقل 72 پوینت بالای ته صفحه میگذارد، و اگر پاصفحهای باشد بیشتر، پس آن شرط هرگز برقرار نمیشد. هر جدول بلند در چنین سندی بهصورت قطعههای مستقل با ContinuationGroup برابر صفر برمیگشت و فراخوان دوباره دستی به هم میدوختشان. این آزمون همچنان برای همان چیزی که برایش طراحی شده معنا دارد: گزارشهایی که layout engineها تولید میکنند و صفحهای را تا یک جعبهٔ محتوای ثابت پر میکنند و صفحهٔ بعد را از بالا شروع میکنند. قاعدهٔ بدی نیست، قاعدهای ناقص است، و همین دلیل است که نسخهٔ 3.117.0 نگهش داشت و بهجای جایگزین کردن، یک مسیر دوم اضافه کرد
آزمون محتوامحور بهجایش چه چیزی را بررسی میکند؟
آزمون محتوامحور بررسی میکند که آیا چیزی جز جدول فضای میان دو قطعه را اشغال کرده، با استفاده از جعبههای کلمه در هر صفحه نه هندسهٔ صفحه. ExtractDocumentTables حین پیمایش سند، به ازای هر صفحه پایینترین لبهٔ پایینی هر کلمهای که بالایش بالای نوار پاصفحه میافتد و بالاترین لبهٔ بالایی هر کلمهای که پایینش زیر نوار سرصفحه میافتد را ثبت میکند. ضخامت هر دو نوار ContinuationMargin پوینت است، پس همان option حالا دو نقش دارد: فاصلهای برای لبهٔ صفحه و ارتفاع ناحیههای سرصفحه و پاصفحهٔ جاری. یک جفت قطعه وقتی پاس میشود که لبهٔ پایینی قطعهٔ قبلی در سطح پایینترین متن اصلی روی صفحهاش یا پایینتر باشد و لبهٔ بالایی قطعهٔ بعدی در سطح بالاترین متن اصلی صفحهٔ بعد یا بالاتر، هر کدام در محدودهٔ AlignmentTolerance. به زبان ساده: جدول آخرین چیز در صفحهٔ N و اولین چیز در صفحهٔ N+1 بوده، و یک شمارهٔ صفحه یا عنوان سند در نوار حاشیه به حساب نمیآید. این استثنا دلبخواه نیست. ISO 32000-1 §14.8.2.2 سرصفحهها و پاصفحههای جاری را بهعنوان pagination artifact دستهبندی میکند، محتوایی که به خاطر شکست صفحه وجود دارد نه بهرغم آن، و همان ایدهای که به یک reader تگدار اجازه میدهد از آنها بگذرد همان چیزی است که به یک جدول اجازه میدهد از آنها ادامه پیدا کند. مقالهٔ marked content پوشش میدهد که فایلهای تگدار این artifactها را چطور صریحاً اعلام میکنند؛ اینجا این دستهبندی از موقعیت استنتاج میشود، چون بیشتر جدولهای exportشده اصلاً تگ ندارند
این دو آزمون با OR ترکیب میشوند. یک گزارش از layout engine که جدولهایش تا لبهٔ کاغذ میروند اولی را پاس میکند؛ یک export از Word که جدولهایش در حاشیه میایستند دومی را پاس میکند؛ سندی که هر دو را دارد دو بار پاس میشود. فقط بعد از موفقیت یکی از آنها بقیهٔ gateها اجرا میشوند، و به ترتیب ثابت اجرا میشوند: شمارههای صفحه باید مجاور باشند، قطعهٔ بعدی نباید با یک سطر caption شروع شود، و مرزهای ستون باید تا دو برابر AlignmentTolerance که در پیشفرضها 6 پوینت است مطابقت داشته باشند. نام enumeration همان TPdfTableContinuation است با مقدارهای ptcNone و ptcStart و ptcMiddle و ptcEnd. قطعهای که ptcEnd علامت خورده و بعد به صفحهٔ دیگری هم میبندد به ptcMiddle ارتقا پیدا میکند، پس یک جدول سهصفحهای به ترتیب صفحه start و middle و end خوانده میشود. شمارههای گروه از 1 شروع میشوند و 0 یعنی بیلینک، و 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 پوینت عمق
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;
چطور یک سطر caption جلوی بههمچسبیدن دو جدول را میگیرد؟
قطعهای در صفحهٔ بعد که سطر اولش یک سلول کشیدهشده روی همهٔ ستونهاست بهعنوان یک جدول جدید رفتار میشود، هرگز بهعنوان بقیهٔ جدول قبلی. این قاعده وجود دارد چون آزمون محتوامحور بهتنهایی با ولع زیادی میبندد. موردی که رو ش کرد یک فرم از نوع صورتجلسه بود: یک جدول نزدیک ته صفحهٔ 1 تمام میشود، جدول دومی با عرض ستونهای یکسان نزدیک بالای صفحهٔ 2 شروع میشود، جز پاصفحه چیزی میانشان نیست، و ستونها مو به مو مطابقت دارند. زیر آزمون حاشیه این دو هرگز به هم نمیرسیدند چون هیچکدام لبهای را لمس نمیکرد؛ زیر آزمون محتوا فوراً به هم بستند و فرمی با چند بخش به یک شبکهٔ بیسروته تبدیل شد. چیزی که جدا نگهشان میدارد در ساختار سلول دیده میشود. جدول دوم با یک caption بخش مثل "RECIPIENT INFORMATION" باز میشود که بهصورت یک سلول ادغامشدهٔ تکی در تمام عرض چیده شده، و یک ادامهٔ واقعی هرگز این کار را نمیکند، چون آن caption به جدولی تعلق دارد که از قبل در صفحهٔ قبلی شروع شده. TableStartsWithCaptionRow دقیقاً همین را کد میکند: قطعه حداقل دو ستون دارد و سلولی در خود دارد با RowIndex = 0 و ColumnIndex = 0 و ColumnSpan = ColumnCount. این بررسی فقط روی قطعهٔ بعدی اجرا میشود، پس جدولی که سطر caption خودش در صفحهٔ اولش نشسته تحت تأثیر قرار نمیگیرد؛ caption روی صفحهٔ N است و فقط قطعهٔ صفحهٔ N+1 بازرسی میشود
مقایسهٔ ستونی که بعد از آن میآید، TablesHaveMatchingColumns، سختگیرانهتر از «تعداد ستون یکسان» است. موقعیت مرزهای هر قطعه را از مستطیلهای سلول از نو میسازد، مرزهایی که سلولهای ادغامشده پنهانشان کردهاند را درونیابی میکند، و جفت را وقتی هر مرزی بیش از تلورانس لغزیده باشد رد میکند. پس دو جدول چهارستونی با تناسبهای متفاوت بیرون میمانند حتی وقتی همهچیز دیگر همتراز باشد
بر سر سطری که به صفحهٔ بعد سرریز میشود چه میآید؟
یک شبکهٔ خطدار که یک سطرش را به صفحهٔ بعد میبرد حالا تشخیص داده و بسته میشود، به شرط اینکه در یک زنجیرهٔ ادامه جا بگیرد؛ بهتنهایی دور انداخته میشود. مقدار پیشفرض MinRows برابر 2 وجود دارد تا یک جفت خط سرگردان بهعنوان جدول گزارش نشود، ولی سطر آخری که از شکست صفحه هُل داده شده یک سطر واقعی است که کفِ سختِ 2 بیصدا انداختش، و بقیهٔ جدول کامل به نظر میرسید در حالی که نبود. اسکن سطح-سند در سه گام ادارهاش میکند. وقتی DetectContinuations و DetectRuledTables هر دو ست باشند، پاس هر-صفحه آشکارساز خطدار را با کف سطر موقتاً پایینآمده به 1 اجرا میکند، و همین دلیل است که ExtractTables حالا برای شبکههای خطدار MinRows برابر 1 را میپذیرد در حالی که تشخیص بر اساس فضای سفید کف درونی 2 را نگه میدارد. ادامهها روی کل نتیجه علامت میخورند. بعد هر جدولی که از 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 برای آن حالت درست است و برای generatorی که هدرها را تکرار نمیکند غلط. قبل از اینکه روتین را روی یک پوشه رها کنی، یک سند را بررسی کن
قواعد هنوز کجا میایستند
آزمون محتوامحور فقط بهاندازهٔ لایهٔ متنی که میخواند خوب است. روی یک صفحهٔ اسکنشده که اصلاً متن ندارد، کرانههای متن اصلی ثبتشده به مرزهای صفحه برمیگردند، شرط «چیزی در میان نیست» بهصورت بیاثر برآورده میشود، و فقط gateهای سطر caption و ستون باقی میمانند؛ یک شبکهٔ خطدار روی چنین صفحهای باز هم بهعنوان یک اسکلت خالی پیدا میشود، پس زنجیره ممکن است درست بسته شود، ولی هیچچیز دربارهٔ متن اطرافش واقعاً تأیید نشده. اگر این مهم است اول یک لایهٔ متنی اضافه کن. پاصفحههایی که بهصورت تصویر رندر شدهاند نه متن، برای منطق نوار نامرئیاند و به همان دلیل بیضرر
این نوارها یک عدد واحدند. پاصفحهای عمیقتر از ContinuationMargin خطهای پایینیاش را داخل ناحیهٔ اصلی جا میگذارد، که باعث میشود قطعهٔ قبلی دنبالشدهبا-متن به نظر برسد و بایند را بلاک میکند؛ option را تا عمق واقعی نوار بالا ببر، همانطور که مثال اول میکند. خیلی بالا ببر و یک پاراگراف پایانی کوتاه نزدیک ته صفحه به داخل نوار میلغزد و نادیده گرفته میشود، که جدول را به هر چه بعدش بیاید میبندد. قاعدهٔ caption یک شکست آینهای دارد: generatorی که یک بنر ادغامشدهٔ "continued" را بهعنوان سطر اول هر قطعهٔ ادامه مینویسد آن قطعهها را بهعنوان جدول جدید ردشده میبیند، و تنها علاج امروز این است که بعد از هیچ شلکردنی خودت با ContinuationGroup به هم بدوزی، چون این قاعده هیچ کلیدی ندارد
جدولهای تشخیصدادهشده با فضای سفید هیچکدام از این تخفیف تکسطری را نمیگیرند. استراتژی فضای سفید برای دیدن یک جدول اصلاً به دو سطر همتراز نیاز دارد، پس یک جدول بدون خط که یک سطرش سرریز شده باز هم یک سطر کم گزارش میشود. وقتی به این برخوردی، جعبههای کلمهٔ پشت بلوکهای متن ساختیافته و ترتیب خواندن موقعیتهای خام را میدهند تا بازیابیاش کنی. در مجموعهٔ نمونهای که این کار را جلو انداخت، سیزده export از واژهپردازها و مرورگرها، هر پنج سند با جدولهای چندصفحهای واقعی به زنجیرههای تکی بسته شدند و آن فرم صورتجلسه که قبلاً به هم چسبیده بود جدا ماند، که همان معیاری است که این انتشار با آن سنجیده شد، نه یک وعده دربارهٔ هر چیدمانی
علامتگذاری ادامه و قاعدهٔ caption و پاس تکسطری همه در مسیر سطح-سند زندگی میکنند که بیلدهای Delphi و C++Builder و Lazarus شریکش هستند؛ API کامل استخراج جدول در صفحهٔ کامپوننت PDFium برای Delphi توصیف شده