مقاله فنی

تبدیل (Flattening) لینک‌های متن‌غنی XFA به لینک‌های PDF در دلفی

ساختار XFA (معماری فرم‌های XML) منسوخ شده است. استاندارد ISO 32000-1 آن را در بخش 12.7 با این توضیح که از PDF 2.0 حذف شده است ارائه می‌دهد، و نمایشگرهای مدرن موتورهای XFA خود را یکی پس از دیگری کنار می‌گذارند. هیچ‌کدام از این‌ها بایگانی‌ها را خالی نکرده است. فرم‌های ورودی دولتی، درخواست‌های بیمه و صورت‌حساب‌های بانکی برای بخش عمده‌ای از دو دهه به عنوان XFA نوشته شده‌اند و این فایل‌ها امروزه هنوز به صندوق‌های ورودی و پایپ‌لاین‌های اسناد می‌رسند. زمانی که نمایشگری که قبلاً آنها را رندر می‌کرد، این کار را متوقف کند، فرم به یک صفحه خالی با نگهدارندهِ "لطفاً در خواننده دیگری باز کنید" تبدیل می‌شود. راه حل پایدار این است که XFA را به محتوای استاتیک PDF (فرآیند Flatten) تبدیل کنیم تا هر خواننده‌ای بتواند آن را ترسیم کند

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

متن غنی XFA در واقع چگونه به نظر می‌رسد

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

بنابراین موتور فلت‌کننده نمی‌تواند با بلوک به عنوان یک رشته واحد برخورد کند. این موتور باید روی ساختار درون‌خطی (inline) حرکت کند، استایل موثر هر اجرا (run) را با لایه‌بندی CSS درون‌خطی span بر روی فونت پایه عنصر draw مشخص کند و اجراها را یکی پس از دیگری در طول خط طرح‌بندی کند. ابزار HotPDF هر یک از این قطعات طرح‌بندی شده را به عنوان یک رکورد داخلی TXFARichRun مدل‌سازی می‌کند. این رکورد، متن اجرا، استایل حل‌شده‌اش، جعبه اندازه‌گیری شده آن و برای یک لینک، مقدار Href را که به آن اشاره می‌کند حمل می‌کند

طرح‌بندی اجراها از چپ به راست

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

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

// 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;

از یک اجرای پیوند (anchor run) تا یک حاشیه‌نویسیِ لینک PDF

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

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

// 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;

چرا جعبه ضربه (hit box) باید از عرض‌های اندازه‌گیری شده به دست آید

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

تنها لنگرگاه (anchor) قابل اعتماد برای مستطیل، هندسه‌ای است که مرحله طرح‌بندی از قبل تولید کرده است. افستِ هر اجرا و عرضِ اندازه‌گیری شده آن در حین جریان یافتنِ خط، قبل از تغییر شماره گلیف، محاسبه شده‌اند و توضیح می‌دهند که متن از نظر فیزیکی کجا ظاهر می‌شود. بنابراین HotPDF مستطیل لینک را مستقیماً از جعبه قرار داده شدهِ اجرا می‌گیرد، نه از هرگونه جستجوی متن. از آنجایی که در اندازه‌گیری از فونت رندر استفاده شده است، صرف‌نظر از زیرمجموعه‌سازی (subsetting)، کادر درست است. هندسه از رمزگذاری جان سالم به در می‌برد؛ اما متن خیر. این تمام استدلال برای موقعیت‌یابی بر اساس عرضِ اندازه‌گیری شده است، و به همین دلیل است که فلت‌کننده‌ای که سعی می‌کند لینک‌ها را با جستجوی متنی تغییر دهد، مناطق حساس (hit zones) تولید می‌کند که جابه‌جا می‌شوند یا از بین می‌روند

هدایتِ فلت‌سازی از درون کد شما

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

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);

    // Anything the engine could not map is reported, not raised.
    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های قابل استفاده)، یک خط را جمع‌آوری می‌کند. هیچ‌کدام از این موارد استثنا (exception) ایجاد نمی‌کنند، بنابراین یک لیست هشدارهای خالی، مدرک شماست مبنی بر اینکه همه چیز مپ شده است، و یک لیست غیرخالی دقیقاً به شما می‌گوید که کدام موارد اصلی را باید بازرسی کنید. زمانی که XFA خام را به جای یک PDF بارگیری شده به عنوان بایت‌های XDP در اختیار دارید، متد مرتبط با آن یعنی ApplyXFAAsAcroForm این بایت‌ها را مستقیماً می‌گیرد و مسیر کد یکسان و رفتار هشدارهای یکسانی را به اشتراک می‌گذارد. متد مکملِ AddXFAPacket در جهت دیگر عمل می‌کند و یک بسته XFA را درون سندی که می‌سازید تعبیه می‌کند

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

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

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