Technical Article

تخت کردن هایپرلینک‌های متنی غنی XFA به لینک‌های PDF در Delphi

معماری XFA یا XML Forms Architecture منسوخ شده است. استاندارد ISO 32000-1 آن را در بخش ۱۲.۷ با این یادداشت که از PDF 2.0 حذف شده است حمل می‌کند و نمایشگرهای مدرن موتورهای XFA خود را یکی یکی حذف می‌کنند. هیچ‌کدام از این‌ها آرشیوها را خالی نکرده است. فرم‌های ورودی دولتی، درخواست‌های بیمه و صورت‌حساب‌های بانکی برای بخش عمده‌ای از دو دهه به صورت XFA نگارش شده‌اند و این فایل‌ها هنوز هم امروزه به اینباکس‌ها و خطوط لوله سند می‌رسند. وقتی نمایشگری که برای رندر کردن آن‌ها استفاده می‌شد دیگر این کار را انجام نمی‌دهد، فرم به یک صفحه خالی با یک متن جایگزین «لطفاً در یک خواننده متفاوت باز کنید» تبدیل می‌شود. راه حل دائمی، تخت کردن XFA به محتوای ایستای PDF است که هر خواننده‌ای بتواند آن را رسم کند.

بخش سخت این تخت کردن، فیلدها نیستند. کادرهای متنی و چک‌باکس‌ها به اندازه کافی تمیز به ویجت‌های AcroForm نگاشت می‌شوند. بخش سخت، متن غنی است که XFA در داخل یک عنصر ترسیم، در یک بلاک <exData contentType="text/html"> ذخیره می‌کند. آن بلاک یک زیرمجموعه HTML با استایل‌دهی درون‌خطی و اغلب، لنگرها (anchors) است. قرار دادن آن روی صفحه به معنای بازتولید متن استایل‌دهی شده و هایپرلینک‌های زنده است و هایپرلینک‌ها جایی هستند که بیشتر پیاده‌سازی‌ها بی‌سروصدا در آن تسلیم می‌شوند.

متن غنی XFA واقعاً چه شکلی است

یک بدنه exData بخش کوچکی از XHTML است. یک پاراگراف به صورت <p> است؛ یک محدوده کاراکتری استایل‌دهی شده به صورت <span> با CSS درون‌خطی خود برای ضخامت، وضعیت، رنگ و اندازه است؛ و یک هایپرلینک به صورت <a href="..."> است که متن مرئی خود را می‌پوشاند. یک خط واحد می‌تواند چندین span را پشت سر هم با استایل‌های متفاوت نگه دارد و یکی از آن‌ها می‌تواند یک لنگر باشد. استایل‌دهی تزئینی نیست که بتوان آن را رها کرد. بندهای رندر شده با رنگ قرمز پررنگ به دلیل اینکه یک هشدار قانونی است، باید پس از تخت شدن به صورت پررنگ و قرمز باقی بمانند، در غیر این صورت سند تخت شده نسخه اصلی را به اشتباه ارائه می‌دهد.

So the flatten engine cannot treat the block as one string. It has to walk the inline structure, resolve each run's effective style by layering the span's inline CSS over the draw element's base font, and lay the runs out one after another across the line. HotPDF models each of these laid-out fragments as an internal TXFARichRun record. The record carries the run's text, its resolved style, its measured box, and, for an anchor, the Href it points at.

چیدن بخش‌ها از چپ به راست

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

به همین دلیل است که تراز فونت اندازه‌گیری اهمیت زیادی دارد. دوری چیدمان مقادیر پیشروی (advances) را اندازه‌گیری می‌کند؛ یک مرحله رندر جداگانه گلیف‌ها را رسم می‌کند. اگر این دو مرحله در مورد فونت اختلاف داشته باشند، جعبه‌هایی که چیدمان محاسبه کرده است در زیر گلیف‌هایی که رندرر رسم می‌کند قرار نخواهند گرفت. HotPDF آن‌ها را با نگاشت استایل حل‌شده هر بخش به یک مشخصه فونت، از طریق هلپر داخلی RunStyleToFontSpec، که با پیش‌فرض‌های خود رندرر یعنی Arial در ۱۰ امتیاز مطابقت دارد، هماهنگ نگه می‌دارد. پیشروی اندازه‌گیری شده و متن ترسیم‌شده سپس با هم توافق دارند و جعبه محاسبه‌شده یک بخش واقعاً کاراکترهایی را که خواننده می‌بیند پوشش می‌دهد.

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

از یک بخش لنگر به یک حاشیه‌نویسی لینک PDF

یک هایپرلینک در یک PDF نهایی بخشی از محتوای صفحه نیست. این یک شیء مجزا، یعنی یک حاشیه‌نویسی لینک (Link annotation) است که در ISO 32000-1 بخش ۱۲.۵.۶.۵ توصیف شده است. حاشیه‌نویسی دارای یک /Rect است که مستطیل قابل کلیک روی صفحه را تعریف می‌کند و اکشنی که هنگام کلیک روی مستطیل اجرا می‌شود. برای یک لینک خارجی، اکشن یک اکشن URI است: /S /URI با آدرس هدف به عنوان رشته /URI آن. متن مرئی زیرین محتوای معمولی صفحه است؛ حاشیه‌نویسی منطقه حساس نامرئی است که روی آن قرار گرفته است.

مسیر تخت‌سازی دقیقاً از همین مدل پیروی می‌کند. وقتی یک بخش Href را حمل می‌کند، HotPDF ابتدا متن استایل‌دهی شده را ترسیم می‌کند، سپس یک حاشیه‌نویسی لینک روی جعبه بخش می‌سازد. نقطه ورود عمومی برای آن حاشیه‌نویسی، متد صفحه AddURILink است که شیء /Type /Annot /Subtype /Link را با یک اکشن /URI ایجاد می‌کند و دیکشنری حاشیه‌نویسی را برمی‌گرداند. مستطیل آن جعبه اندازه‌گیری شده بخش است که از مختصات محلی عنصر ترسیم به مختصات صفحه ترجمه شده است. نتیجه لینکی است که دقیقاً روی متن لنگر قرار می‌گیرد و نه جای دیگر.

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

چرا جعبه کلیک باید از عرض‌های اندازه‌گیری شده به دست آید

وسوسه‌انگیز است که مکان یابی لینک را با جستجوی صفحه برای متن مرئی آن و ترسیم مستطیل در اطراف هر چه یافت می‌شود تصور کنیم. این کار انجام نمی‌شود و دلیل آن در نحوه ذخیره‌سازی متن تخت شده اساسی است. بخش‌های استایل‌دهی شده با فونت‌های زیرمجموعه تعبیه‌شده (embedded subset fonts) ترسیم می‌شوند. یک فونت زیرمجموعه گلیف‌هایی را که نگه می‌دارد مجدداً شماره‌گذاری می‌کند، بنابراین جریان محتوای صفحه کدهای هگزادسیمال CID را نگه می‌دارد، نه کدهای کاراکتر اصلی را. بایت‌های روی صفحه حروفی نیستند که یک انسان می‌خواند و به عنوان متن قابل جستجو نیستند. جستجو برای عنوان لنگر چیزی پیدا نمی‌کند، زیرا آن عنوان به عنوان متن واقعی در هیچ جای جریان وجود ندارد.

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

هدایت تخت‌سازی از کد شما

برای یک PDF که از قبل حاوی پکت XFA است، نقطه ورود FlattenLoadedXFA است. سند را بارگذاری کنید، متد را فراخوانی کرده و نتیجه را ذخیره کنید. پارامتر Editable تصمیم می‌گیرد که چه اتفاقی برای فیلدهای فرم بیفتد: مقدار True را پاس دهید تا آن‌ها را به عنوان ویجت‌های AcroForm قابل پر کردن نگه دارید، یا False تا هر ویجت را فقط‌خواندنی علامت‌گذاری کنید تا خروجی یک سند ثابت باشد. بلاک‌های ترسیم متن غنی با بخش‌های استایل‌دهی شده و حاشیه‌نویسی‌های لینک آن‌ها در هر دو حالت تولید می‌شوند. تابع تعداد ویجت‌های صادرشده را برمی‌گرداند.

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

همیشه پس از فراخوانی، XFAFlattenWarnings را بخوانید. لیست در شروع هر تخت‌سازی پاک می‌شود و برای هر عنصری که موتور از رندر کردن آن خودداری کرده است، یک خط جمع می‌کند: نوع فیلد پشتیبانی‌نشده، تصویر ترسیمی که رمزگشایی نمی‌شود، بلاک exData بدون spanهای قابل استفاده. هیچ‌کدام از آن‌ها خطایی ایجاد نمی‌کنند، بنابراین یک لیست هشدار خالی مدرک شماست مبنی بر اینکه همه چیز نگاشت شده است و یک لیست غیرخالی به شما می‌گوید دقیقاً کدام نسخه‌های اصلی را بررسی کنید. وقتی XFA خام را به عنوان بایت‌های XDP به جای یک PDF بارگذاری شده نگه می‌دارید، متد هم‌خانواده ApplyXFAAsAcroForm آن بایت‌ها را مستقیماً می‌گیرد و در همان مسیر کد و رفتار هشدارها شریک می‌شود. متد مکمل AddXFAPacket در مسیر عکس عمل می‌کند و یک پکت XFA را به سندی که در حال ساخت آن هستید اضافه می‌نماید.

تایید نتیجه در یک خواننده

فایل تخت شده را در Acrobat یا هر نمایشگر فعلی باز کنید و دو چیز را بررسی نمایید. اول اینکه متن غنی با استایل دست‌نخورده خود رندر شده باشد: بخش‌های پررنگ، پررنگ هستند، بخش‌های رنگی رنگ خود را حمل می‌کنند و spanها به ترتیب درست روی خط قرار گرفته‌اند به جای اینکه همپوشانی داشته باشند یا از جعبه خارج شوند. دوم اینکه هایپرلینک‌ها زنده باشند. نشانگر را روی یک لنگر ببرید و نوار وضعیت باید آدرس هدف را نشان دهد؛ روی آن کلیک کنید و اکشن URI باید آن را باز کند. از بازرس حاشیه‌نویسی نمایشگر استفاده کنید تا تایید کنید هر کدام یک حاشیه‌نویسی واقعی /Link است که /Rect آن متن لنگر را در آغوش می‌گیرد، و روی محتوایی قرار گرفته که اکنون گلیف‌های ترسیم‌شده ساده هستند به جای XFA رندر شده با فرم. این ترکیب، متن ایستای استایل‌دهی شده به علاوه حاشیه‌نویسی‌های واقعی لینک روی مستطیل‌های مناسب، چیزی است که باعث می‌شود سند تخت شده بیشتر از موتورهای XFA که دیگر به آن‌ها نیاز ندارد عمر کند.

تخت کردن خود فیلدها، کادرهای متنی، چک‌باکس‌ها و لیست‌های انتخابی که این متن غنی را احاطه کرده‌اند، در راهنمای ما در مورد تخت کردن فرم‌های XFA به ویجت‌های AcroForm پوشش داده شده است. برای داستان گسترده‌تر ساخت و قرار دادن دستی حاشیه‌نویسی‌های لینک، فراتر از مواردی که مسیر تخت‌سازی تولید می‌کند، به کار با حاشیه‌نویسی‌های PDF در HotPDF مراجعه کنید. هر دو بر روی همان مدل حاشیه‌نویسی و فرم‌ها ساخته شده‌اند که با کامپوننت HotPDF برای Delphi و C++Builder ارائه می‌شود.