مقاله فنی

چیدمان اعلانی PDF در دلفی با خروجی برچسب‌گذاری‌شده (Tagged)

HotPDF می‌تواند یک سند صفحه‌بندی‌شده را از یک درخت اعلانی بسازد، نه از مختصات. شما یک THPDFDOMDocument را از بخش‌ها، پشته‌ها، متن، فهرست‌ها و جدول‌ها مونتاژ می‌کنید، آن را به THPDFDOMRenderer می‌سپارید، و رندرکننده اندازه‌گیری می‌کند، صفحه‌بندی می‌کند، اثاثیه صفحه را ترسیم می‌کند، و در صورت درخواست، درخت ساختار PDF/UA را که نتیجه را قابل‌دسترس می‌کند منتشر می‌کند. کد چیدمان هرگز یک مختصات y محاسبه نمی‌کند

هر کسی که یک موتور تولید گزارش مختصات‌محور را نگهداری کرده باشد، می‌داند چرا این اهمیت دارد. نسخه اول کار می‌کند. سپس نشانی یک مشتری به سه خط رشد می‌کند، یک جدول ردیف‌های بیشتری می‌گیرد، یک عنوان محلی‌سازی‌شده می‌شکند، و هر موقعیت y پایین‌دستی نادرست می‌شود. رفع این مشکلات به‌صورت بررسی‌های دستی شکست صفحه که در سراسر منطق کسب‌وکار پراکنده‌اند انباشته می‌شود، و الزام PDF برچسب‌گذاری‌شده که دو سال بعد می‌رسد نمی‌تواند روی کدی که هیچ تصوری از یک پاراگراف ندارد بازساخت شود

درخت مالک چه چیزی است، و چرا مالکیت سخت‌گیرانه است؟

DOM مالکیت واحد را در هر سطح اجرا می‌کند: سند مالک بخش‌های خود است، یک بخش مالک بدنه، سربرگ و پابرگ خود است، و پشته‌ها، ظرف‌ها و جدول‌ها مالک فرزندان خود هستند. استفاده مجدد از طریق Clone یا از طریق یک کارخانه (factory) ثبت‌شده اتفاق می‌افتد، هرگز با متصل‌کردن یک شیء واحد به دو والد. این قاعده تشریفاتی نیست. مؤلفه‌ای که دوبار در درخت ظاهر شود، با محدودیت‌های متفاوت دوبار اندازه‌گیری می‌شود و در زمان تخریب دوبار آزاد می‌شود

پیامد عملی برای کد فراخوانی‌کننده این است که کمک‌تابع‌ها نمونه‌های جدید برمی‌گردانند. ثبت یک کارخانه با RegisterComponent و فراخوانی CreateComponent به شما یک دستورالعمل نام‌گذاری‌شده می‌دهد که هر بار یک مؤلفه تازه تولید می‌کند، که همان روشی است که اثاثیه تکرارشونده مانند یک بلوک امضا یا یک پابرگ حقوقی متعلق به درخت است

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // انتشار درخت ساختار PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 بر حسب پوینت
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // وزن‌ها، نه عرض‌های مطلق
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // ردیف سربرگ
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

صفحه‌بندی چگونه از هزینه درجه‌دوم اجتناب می‌کند؟

روش ساده‌لوحانه برای صفحه‌بندی یک درخت این است که هر چیزی را که جا نشده کلون کنید و به صفحه بعدی ببرید. روی یک جدول با ده هزار ردیف، این کار ردیف‌های باقی‌مانده را یک‌بار به ازای هر صفحه کلون می‌کند و یک سند خطی را به یک سند درجه‌دوم تبدیل می‌کند

در عوض HotPDF به‌صورت باریک تقسیم می‌کند. رندرکننده سطح‌بالا فرزندان بدنه را بر اساس اندیس می‌پیماید و هرگز یک بخش یا بدنه کامل را کلون نمی‌کند. فقط پشته‌ها و ظرف‌های تودرتویی که واقعاً بر مرز یک صفحه پهن می‌شوند، زیردرخت متأثرشان کلون می‌شود، و دو نوع برگ سنگین یک مکان‌نما (cursor) حمل می‌کنند نه یک کپی: یک تداوم متن، بازه نویسه‌ای از منبع را که هنوز بدهکار است ذخیره می‌کند، و یک تداوم جدول، برش ردیفی را که هنوز باید جای‌گذاری کند ذخیره می‌کند. اسناد طولانی خطی باقی می‌مانند، و پاراگراف‌های طولانی چه یک‌بار بشکنند چه پنج‌بار، هزینه یکسانی دارند

اندازه‌گیری در مورد اثرات جانبی صادق باقی می‌ماند. THPDFLayoutElement.Measure باید عاری از اثرات جانبی ترسیمی باشد، و جای‌گذاری واقعی همیشه از طریق THotPDF.PlaceLayoutElement اجرا می‌شود، همان روال مرکزی‌ای که قطعه جای‌گذاری‌شده را دوباره اندازه‌گیری می‌کند، مالکیت سرریز را برپا می‌کند و تشخیص‌ها را ثبت می‌کند. رندرکننده DOM فقط سیاست صفحه تازه، اثاثیه صفحه، فاصله‌گذاری و طول عمر تداوم‌ها را تصمیم می‌گیرد

قواعد سربرگ جدول که از یک سند بی‌پایان جلوگیری می‌کنند

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

صفحات تداوم، سربرگ را دوباره ترسیم می‌کنند، و آن کپی دوباره‌ترسیم‌شده به‌عنوان یک مصنوع (artifact) به‌جای محتوا علامت‌گذاری می‌شود، که پاسخ درست هم برای دسترس‌پذیری و هم برای استخراج متن است. ردیف سربرگ اصلی دقیقاً یک‌بار در ساختار منطقی جدول باقی می‌ماند. اگر این را نادیده بگیرید، یک صفحه‌خوان (screen reader) عنوان‌های ستون را دوباره در میانه داده اعلام می‌کند، و یک استخراج‌کننده متن یک ردیف سربرگ تکراری بین ردیف‌های بدنه درج می‌کند

یک سقف دفاعی نیز روی عمق تداوم وجود دارد، زیرا یک مؤلفه سفارشی آزاد است که Split را به روشی پیاده‌سازی کند که همیشه یک دنباله معادل برمی‌گرداند. رندرکننده حد را پس از جداکردن دنباله و پیش از شروع صفحه بعدی بررسی می‌کند، و تکرار جاری دنباله را در بلوک finally خودش آزاد می‌کند، بنابراین یک مؤلفه شخص‌ثالث بدرفتار با یک خطای قابل‌تشخیص شکست می‌خورد نه با پرکردن یک دیسک

یک عنصر منطقی، چندین قطعه صفحه

برچسب‌گذاری خودکار جایی است که مدل صفحه‌بندی و مدل ساختار باید با هم توافق کنند. یک پاراگراف که در دو صفحه شکسته شده، یک پاراگراف منطقی واحد است، بنابراین باید یک عنصر ساختاری واحد باقی بماند. اما شناسه‌های محتوای علامت‌گذاری‌شده (marked content) به ازای هر صفحه هستند، بنابراین هر قطعه قابل‌مشاهده به MCID مخصوص به خود در صفحه‌ای که در آن ظاهر می‌شود نیاز دارد

HotPDF این را با نگه‌داشتن یک عنصر ساختاری واحد و افزودن یک ارجاع محتوای علامت‌گذاری‌شده به آرایه /K آن به ازای هر قطعه حل می‌کند، که در آن جفت /Pg و /MCID صفحه و شناسه را مشخص می‌کنند. جایگاه ParentTree برای آن MCID به همان عنصر بازمی‌گردد. این دقیقاً همان چیزی است که ISO 14289 انتظار دارد، و همین دلیل است که کلون‌های تداوم از کلون‌های معمولی متمایزند: یک Clone معمولی به‌معنای محتوای منطقی جدید است و یک هویت معنایی جدید می‌گیرد، درحالی‌که کلون تداوم داخلی، هویت مؤلفه‌ای را که ادامه می‌دهد به ارث می‌برد

استفاده مجدد از عنصر از طریق یک نمایه از هویت‌های معنایی که بر اساس اشاره‌گر مؤلفه مرتب شده و با مقایسه دودویی جست‌وجو می‌شود، انجام می‌گیرد، که جست‌وجو را روی درخت‌های بزرگ لگاریتمی نگه می‌دارد. نمایه فقط ارجاعات غیرمالک را نگه می‌دارد؛ طول عمر خود اشیای ساختاری با گراف اشیای PDF می‌ماند

قواعد ساختاری که رندرکننده از پیش اجرا می‌کند

با فعال‌بودن GenerateStructure، چندین قاعده PDF/UA در حین رندرشدن درخت بررسی می‌شوند، نه پس از وجودیافتن فایل. عنوان‌ها از سطح ۱ شروع می‌شوند و نمی‌توانند سطح را رد کنند. LI فقط می‌تواند درون L ظاهر شود، و Lbl و LBody فقط درون LI. TR متعلق به یک جدول است، و TH و TD متعلق به یک ردیف. یک شکل بدون متن جایگزین در حالت PDF/UA رد می‌شود

رد‌کردن زودهنگام همان انتخاب عمدی در اینجاست. یک اعتبارسنجی که پس از نوشته‌شدن سند، فقدان متن جایگزین را گزارش می‌دهد، به شما می‌گوید یک دسته ده‌هزارتایی از صورت‌حساب‌ها باید دوباره تولید شود؛ رندرکننده‌ای که مؤلفه را رد می‌کند به شما می‌گوید کدام مؤلفه، درحالی‌که داده‌ای که آن را تولید کرده هنوز در دامنه است. تأیید انطباق همچنان به‌عنوان یک گام جداگانه در خط‌لوله باقی می‌ماند، و مکانیزم‌های آن در تأیید PDF/A، PDF/X و PDF/UA پوشش داده شده

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

رکورد آماری در نگاه اول به نظر می‌رسد از آنچه هست مفیدتر است. افزایش شدید SplitCount پس از تغییر یک قالب معمولاً به این معناست که یک مؤلفه شروع کرده از ظرف خود بلندتر اندازه‌گیری شود. خزیدن روبه‌بالای MaximumContinuationDepth هشدار زودهنگام برای مؤلفه‌ای است که Split آن به ازای هر صفحه پیشرفت بسیار کمی می‌کند. و مقایسه ArtifactCount با تعداد صفحات تداوم تأیید می‌کند که سربرگ‌های تکرارشده واقعاً به‌عنوان مصنوع برچسب‌گذاری شده‌اند

DOM کجای کنار API مستقیم جای می‌گیرد؟

DOM جایگزین ترسیم مستقیم نمی‌شود؛ روی همان اشیای صفحه می‌نشیند. هر چیزی که رندرکننده جای می‌دهد می‌تواند با فراخوانی‌های مستقیم روی THotPDF درهم‌تنیده شود، که وقتی یک گزارش به یک عنصر دستی‌جانمایی‌شده مانند یک تصویر امضا در یک موقعیت دقیق نیاز دارد، اهمیت دارد. بستن صفحه همچنان تحت کنترل AddPage و EndDoc باقی می‌ماند، بنابراین حالت تخلیه فوری هیچ صفحه تکمیل‌شده‌ای را در حافظه نگه نمی‌دارد و حافظه مقیم تحت حاکمیت تداوم‌های جاری، منابع فونت و گراف اشیای معمول سند باقی می‌ماند

وقتی محتوا داده‌محور و چیدمان قاعده‌محور است، DOM را انتخاب کنید، و ترسیم مستقیم را برای آثار هنری ثابت نگه دارید. اگر دردسر فعلی شما به‌طور خاص صفحه‌بندی جدول است، رویکرد باریک‌تر در تولید جدول در PDF ارزش خواندن اول را دارد، و رفتار سطح متن مانند تراز، در تراز متن شرح داده شده

چیدمان اعلانی، برچسب‌گذاری خودکار و API ترسیم مستقیم در یک مؤلفه واحد برای دلفی و C++Builder عرضه می‌شوند؛ فهرست کامل ویژگی‌ها در صفحه مؤلفه HotPDF PDF برای دلفی قرار دارد