مقاله فنی

گریدهای Rowspan و هدرهای جدول تکرارشونده در HotPDF

HotPDF جدول‌های HTML را از طریق پروفایل paged-media خود HTML5 رندر می‌کند، با یک گرید اشغال واقعی برای rowspan و colspan، ارتفاع ردیف‌های اندازه‌گیری‌شده به‌جای تخمین شمارش کاراکتر، و ردیف‌های هدر که در هر صفحه ادامه تکرار می‌شوند. دو موقعیت هست که باعث می‌شود از تکرار هدر خودداری کند، و دانستن‌شان از قبل ارزان‌تر از دیباگ یک سلول تکراری است

رده سندی که این را الزامی می‌کند همان است که هر تیم گزارشگری بالاخره عرضه می‌کند: یک فاکتور یا گزارش انطباق که منبع حقیقتش HTML است، جدولش چهار صفحه ادامه پیدا می‌کند، و هدر باید در تک‌تک آن‌ها خوانا باشد. هر چیزی کمتر از یک چیدمان جدول واقعی همان دو شکستی تولید می‌کند که خواننده فوراً می‌بیند: هدری که فقط یک بار در صفحه یک ظاهر می‌شود و ردیف‌هایی که ارتفاعشان از شمارش کاراکتر حدس زده شده

چرا قابلیت جدول به HTML renderer منتقل شد؟

چون گزینه دیگر rich text را از دست می‌دهد، و rich text همان دلیلی است که محتوا از اول HTML است. طرح بدیهی شبیه استفاده مجدد است: HotPDF از قبل یک آبجکت جدول layout DOM با گرید درست دارد، پس parser HTML را به آن پل بزنید و spanning را مجانی بگیرید. مشکل این است که آن آبجکت جدول با چه چیزی می‌کشد. سلول‌هایش متن و یک style حمل می‌کنند و مسیر رسم‌شان خروجی متن ساده می‌دهد، پس هر چیزی که HTML واقعاً در دل داشت فراتر از یک فونت و یک رنگ — لینک‌ها، بالانویس‌ها، تغییر اندازه درون‌خطی، رنگ به‌ازای هر run — تا به صفحه برسد از دست رفته

جهتی که با اسناد واقعی سینه‌سینه می‌ماند جهت معکوس است. قابلیت‌های موتور جدول — گرید اشغال، اندازه‌گیری واقعی، تکرار هدر و وزن‌دهی ستون — را به HTML renderer منتقل کنید و رندر rich-text را همان‌جا که از قبل کار می‌کند باقی بگذارید. این تغییر بزرگ‌تری از پل است، و همان تغییری است که یک hyperlink داخل سلول جدول را hyperlink نگه می‌دارد

Rowspan بدون union-find

سلول‌های spanدار گروه‌های ردیف اتمی می‌سازند، ولی بستن روی این گروه‌ها هیچ ساختار disjoint-set عمومی نمی‌خواهد، چون اشغال همیشه یک بازه پیوسته است. سلولی با rowspan="3" که از ردیف K شروع می‌شود ردیف‌های K تا K+2 را اشغال می‌کند و هیچ‌چیز دیگر را، پس اطلاعات گروه به یک نشانگر پایان به‌ازای هر ردیف ساده می‌شود

الگوریتم دو خط نیت است. وقتی سلول spanداری را جا می‌دهید که از K شروع و در E تمام می‌شود، ثبت کنید GroupEnd[K] := Max(GroupEnd[K], E). بعد ردیف‌ها را یک بار وارونه بگردید و G[R] := G[G[R]] را اعمال کنید، که هر پایان ردیف را از پشت روی spanهای هم‌پوشان منتشر می‌کند و بستن تراگذری را در یک گذر تحویل می‌دهد. چیزی که می‌گیرید این است که برای هر ردیف، آخرین ردیفی که باید با آن در یک صفحه بماند، که دقیقاً همان چیزی است که گام صفحه‌بندی برای تصمیم‌گیری درباره اینکه شکست کجا مجاز است لازم دارد

توزیع ارتفاع نیمه دیگر است. وقتی یک سلول spanدار فضای عمودی بیشتری از آنچه ردیف‌های تحت پوششش فعلاً فراهم می‌کنند لازم دارد، مازاد به ردیف آخر span می‌رود، نه اینکه به‌طور مساوی پخش شود. سلول‌های spanدار را بعد از جاافتادن ارتفاع ردیف‌های معمولی پردازش کنید، بعد ردیف پایانی هر span را پر کنید. پخش مساوی مازاد عادلانه‌تر به نظر می‌رسد و خروجی به‌وضوح غلط تولید می‌کند: ردیف‌هایی که فقط سلول‌های تک‌خطی کوتاه دارند باد می‌کنند چون یک سلول بی‌ربط سه ردیف بالاتر اتفاقاً قدبلند بوده

یک گرید جدول HTML در HotPDF که یک سلول با rowspan 3 شروع‌شده از ردیف ۲ ردیف‌های ۲ تا ۴ را به‌عنوان یک مستطیل اتمی واحد اشغال می‌کند، در کنار مقادیر پایان گروه به‌ازای هر ردیف که از یک گردش وارونه تولید شده و نشان می‌دهد ردیف‌های ۲، ۳ و ۴ به یک صفحه مقیدند
اشغال spanدار همیشه یک بازه پیوسته است، پس نشانگرهای پایان به‌ازای هر ردیف و یک گردش وارونه جای union-find را می‌گیرند و به صفحه‌بندی دقیقاً می‌گویند شکست کجا مجاز است
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Stats: THPDFHTMLImportStatistics;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'audit-report.pdf';
    Pdf.BeginDoc;
    Importer := THPDFHTMLImporter.Create(Pdf);
    try
      Importer.Margin := 48;
      Importer.BaseFontName := 'Arial';
      Importer.BaseFontSize := 10;
      Importer.MaxDOMNodes := 200000;
      Importer.MaxLayoutOperations := 2000000;
      if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
      begin
        Stats := Importer.Statistics;
        Writeln('tables ', Stats.TableCount,
                '  page breaks ', Stats.PageBreakCount);
      end;
    finally
      Importer.Free;
    end;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

RenderHTML5 یک style sheet نویسنده اختیاری به‌عنوان آرگومان دوم می‌گیرد، که جای قواعد چاپ همین‌جاست. style sheet صفحه نمایش را داخلش نیاورید. پروفایل نسخه‌بندی شده و HTML5ProfileMilestones گزارش می‌دهد بیلد جاری کدام گروه‌های قابلیت را پیاده کرده، himParserCascade، himPagedLayout، himTablesForms و himBoundedResources، تا برنامه بتواند عمداً افت کند به‌جای اینکه یک شکاف را در production کشف کند

اندازه‌گیری باید دقیقاً با رسم هم‌نظر باشد

ارتفاع ردیف فقط وقتی درست است که کدی که خطوط شکسته را اندازه می‌گیرد با همان قاعده‌ای بشکند که کد رسم‌کننده می‌شکند. این بدیهی به نظر می‌رسد و رایج‌ترین منبع تکِ جدول‌هایی است که حاشیه‌هایشان با محتوایشان خط نمی‌افتد. HotPDF با یک شمارنده خط حریصانه اندازه می‌گیرد، و آن شمارنده باید در سه جنبه مشخص با معناشناسی شکست خط مسیر خروجی rich-text جور باشد: فقط در فاصله‌ها می‌شکند، هرگز یک کلمه را نمی‌بُرد، و کلمه‌ای که از ستون پهن‌تر است یک خط جداگانه می‌گیرد

الزام دوم فونت است. اندازه‌گیری باید با فونت خود سلول اجرا شود، با SetFont و نام واقعی، مجموعه style و اندازه، پیش از فراخوانی تابع پهنا، نه با هر فونتی که اتفاقاً فعال بوده. متن Bold در همان اندازه معمولاً بیش از ده درصد پهن‌تر از regular است، که برای تبدیل سلول سه‌خطی به سلول چهارسطخی کافی است. جدولی که سلول‌های هدرش bold است و سلول‌های بدنه نیست، اگر با یک فونت اندازه گرفته شود، دقیقاً در ردیف‌هایی غلط درمی‌آید که خواننده اول به آن‌ها نگاه می‌کند

درست‌گرفتن این‌ها تغییر می‌دهد که در تست چه چیزی را می‌توانید assert کنید. اثر قابل مشاهده اندازه‌گیری دقیق فاصله خطوط است نه شمار گلیف‌ها: یک ردیف تک‌خطی حدود ۲۰ پوینت قد دارد در حالی که تخمین شمارش کاراکتر برای همان محتوا دو خط و حدوداً ۳۵ پیش‌بینی می‌کند. روی فاصله عمودی بین ردیف‌ها assert کنید. و یادتان باشد فضای کاربری PDF دارای Y صعودی است، پس هدری که بالای یک ردیف بدنه نشسته یعنی مقدار Y هدر بزرگ‌تر است، که برعکس غریزه مختصات صفحه‌نمایش است

HotPDF کی از تکرار هدر خودداری می‌کند؟

در دو مورد، که هر دو اگر ادامه می‌دادند خروجی به‌وضوح غلط تولید می‌کردند. اولین، یک بلاک هدر شامل سلول spanداری است که از هدر فراتر به ردیف‌های بدنه می‌رود. تکرار هدر محتوای آن سلول را بار دوم در موقعیتی می‌کشید که دیگر به آن تعلق ندارد، پس هدر یک بار کشیده می‌شود و جدول بدون آن ادامه پیدا می‌کند. دومین، هدری است که بلندتر از ۹۰ درصد ارتفاع مفید صفحه است، جایی که تکرار تقریباً هیچ جایی برای داده باقی نمی‌گذارد و جدول هیچ پیشرفتی نمی‌کند

جریان تصمیم HotPDF برای تکرار هدرهای جدول HTML در شکست صفحات: هدری که rowspan آن به ردیف‌های بدنه نفوذ می‌کند یک بار کشیده می‌شود، هدری که بلندتر از ۹۰ درصد ارتفاع مفید صفحه است یک بار کشیده می‌شود، و هر هدر دیگری در هر صفحه ادامه تکرار می‌شود
دو خودداری عمدی‌اند: تکرار هدری که مالک یک سلول spanدار بدنه است یا بیشتر صفحه را پر می‌کند، محتوا را جایی می‌کشد که دیگر به آن تعلق ندارد یا هیچ جایی برای داده باقی نمی‌گذارد

هر دو خودداری عمدی و بی‌سروصدا به‌صورت طراحی‌شده‌اند، چون گزینه دیگر بدتر است. اگر هدرتان تکرار نمی‌شود و انتظار داشتید شود، پیش از اینکه به موتور مشکوک شوید، markup را برای یک rowspan که مرز thead را قطع می‌کند چک کنید. همین یک الگوی markup حساب بیشتر سورپرایزها را می‌دهد

// وزن ستون‌ها از markup می‌آید، پس style sheet چاپ جای کنترل‌شان است.
// پهناها به‌عنوان وزن پردازش می‌شوند، نه به‌عنوان پیکسل
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// ردیف هدری که rowspanای دارد که به بدنه نفوذ می‌کند تکرار هدر را
// سرکوب می‌کند. spanها را درون یک بخش نگه دارید:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  اوکی
//   <tr><th rowspan="3">Item</th>...  نفوذ به tbody، بدون تکرار

پهنای ستون‌ها مثل وزن رفتار می‌کنند نه اندازه‌گیری مطلق، که رفتاری است که جدول را وقتی محتوا با تخمین نویسنده جور نیست کاربردی نگه می‌دارد. ستونی که ۳۰ درصد اعلام شده تقریباً ۳۰ درصد عرض موجود را می‌گیرد، ولی توزیع به حداقل پهنایی که هر ستون واقعاً لازم دارد احترام می‌گذارد، پس ستون باریکی که یک توکن طولانی نشکن را حمل می‌کند بی‌سروصدا جعبه جدول را سرریز نمی‌کند

جای این در خط لوله سند کجاست

کار جدول درون پروفایل گسترده‌تر paged-media می‌نشیند، و قواعد صفحه‌بندی، بودجه‌های resource و رفتار CSS که در مسیر واردات paged-media در HTML5 توصیف شد بدون تغییر روی اسنادی که جدول دارند هم اعمال می‌شود. اگر دیتای شما از HTML شروع نمی‌شود، مسیر ساخت مستقیم در ساخت مستقیم جدول در PDF لایه parsing را کلاً دور می‌زند و همان رفتار گرید را از طریق یک API می‌دهد. و چون ارتفاع ردیف در نهایت به اینکه خطوط کجا می‌شکنند وابسته است، بحث اندازه‌گیری در justify متن و شکست خط قطعه همراه هر کسی است که خروجی جدولی متراکم را تنظیم می‌کند

درس قابل استفاده مجدد اینجا اصلاً درباره جدول‌ها نیست. وقتی یک زیرسیستم جدید قابلیتی می‌خواهد که یک زیرسیستم قدیمی از قبل دارد، بپرسید کدام‌یک از این دو مالک سخت‌ترین چیز برای بازپیاده‌سازی است. حساب گرید چند ده خط است و به‌راحتی جابه‌جا می‌شود. رندر rich-text با لینک درون‌خطی، بالانویس و استایل به‌ازای هر run چنین نیست، پس گرید منتقل شد و متن ماند. HotPDF هر دو مسیر را به‌عنوان بخشی از HotPDF Delphi PDF component عرضه می‌کند، پس انتخاب بین ورودی HTML و ساخت مستقیم یک تصمیم پروژه است نه یک تصمیم کتابخانه