مقاله فنی

افزودن فیلدهای AcroForm به یک PDF بارگذاری‌شده در Delphi

شما یک الگوی فاکتور از یک شخص ثالث دارید، یا قراردادی بایگانی‌شده که سال‌ها پیش با نرم‌افزاری تولید شده و دیگر کسی نمی‌تواند پیدایش کند، و حالا باید آن را تعاملی کنید: یک کادر امضا گوشه صفحه بگذارید، چند فیلد متنی اضافه کنید، و یک چک‌لیست تخت را به چک‌باکس‌های واقعی تبدیل کنید. مشکل این است که این PDF را از صفر نمی‌سازید. فایل از قبل وجود دارد، از قبل صفحه‌ها و جریان‌های محتوا و فونت‌هایی دارد که در اختیار شما نیست، و باید بدون بازسازی، widgetهای AcroForm را روی همان گراف شیء سوار کنید. این مسئله با ساخت فرم روی یک سند تازه فرق دارد، و بخشی که معمولاً آدم‌ها را به دردسر می‌اندازد تا وقتی فایل را در یک viewer باز نکنید دیده نمی‌شود، و فیلدهایی که تازه نوشته‌اید اصلاً روی صفحه نیستند

HotPDF یک مؤلفه بومی VCL PDF برای Delphi و C++Builder است، و از نسخه v2.247.0 به بعد یک خانواده اختصاصی از روش‌ها را دقیقاً برای همین کار ارائه می‌کند: ساخت هر شش نوع استاندارد فیلد به‌طور مستقیم روی سندی که با LoadFromFile بارگذاری شده است. این مقاله توضیح می‌دهد این روش‌ها چه می‌کنند، دیکشنری ISO 32000-1 را چگونه می‌سازند، و چرا یک پرچم خاص بدون آن کل فرایند بی‌سروصدا یک فایل خالی‌نما تولید می‌کند

چرا ایجاد فیلد روی سند بارگذاری‌شده مسیر کد جداگانه‌ای دارد

وقتی یک PDF را از صفر می‌سازید، HotPDF مالک کل مدل شیء است. هر صفحه یک THPDFPage wrapper قابل‌نوشتن است، و افزودن یک فیلد متنی از طریق AddTextField ویجت جدید را به شیء annotation صفحه، شیء page، و مجموعه fieldهای فرم متصل می‌کند، سپس یک appearance stream از منابع فونت سند تولید می‌کند. appearance stream سطح قابل‌نمایش ویجت است، یعنی کادر و border و هر متن پیش‌فرض، که به‌صورت PDF drawing operatorهایی رسم می‌شود که viewer عیناً رندر می‌کند

یک سند بارگذاری‌شده هیچ‌یک از آن زیرساخت‌ها را در اختیار شما نمی‌گذارد. صفحه‌ها به صورت دیکشنری‌های خام وارد شده‌اند؛ هیچ THPDFPage wrapper قابل‌نوشتنی نیست که ویجت را روی آن سوار کنید، و مهم‌تر از آن هیچ pipeline منبع فونتی آماده نیست تا appearance streamها را رسم کند. بنابراین مسیر بارگذاری‌شده از راهی متفاوت می‌رود. این مسیر دیکشنری‌های فیلد را مستقیم روی گراف شیء تجزیه‌شده می‌نویسد و به‌جای شیء صفحه، صفحه‌ها را با شاخص صفرمبنا آدرس می‌دهد. نوع فیلد و بیت‌های flag دقیقاً با مسیر ساخت از صفر یکی هستند، پس یک Text field در هر دو حالت همان Text field است؛ آنچه عوض می‌شود لایه زیرین و، مهم‌تر از همه، شیوه رسم سطح ویجت است

پرچم /NeedAppearances در این‌جا اختیاری نیست

این همان نکته‌ای است که تعیین می‌کند کار شما دیده شود یا نه. چون مسیر بارگذاری‌شده appearance stream تولید نمی‌کند، یک ویجت تازه‌افزوده در viewer بدون ورودی /AP ظاهر می‌شود: فیلدی بدون سطح توصیف‌شده. بسیاری از viewerها وقتی بخواهند ویجتی را که appearance ندارد و هیچ دستوری برای ساختن آن هم ندارد رندر کنند، هیچ چیزی نشان نمی‌دهند. فیلد در فایل هست، از نظر ساختاری معتبر است، ابزار پرکردن فرم می‌تواند به آن دسترسی داشته باشد، و برای یک انسان کاملاً نامرئی است

راه خروج در ISO 32000-1 §12.7.3 تعریف شده است: دیکشنری AcroForm یک /NeedAppearances بولی را در خود نگه می‌دارد، و وقتی true باشد، یک reader سازگار باید خودش appearance streamهای گمشده را از رشته و مقدار /DA هر فیلد بسازد. HotPDF این را برای شما تنظیم می‌کند. نخستین بار که هر فیلدی را به یک سند بارگذاری‌شده اضافه می‌کنید، EnsureLoadedAcroForm اجرا می‌شود: اگر catalog هیچ /AcroForm نداشته باشد آن را می‌سازد، اگر هیچ /Fields آرایه‌ای وجود نداشته باشد آن را می‌سازد، و /NeedAppearances true را روی true می‌گذارد. شما مستقیماً آن را صدا نمی‌زنید، اما دانستن وجودش رفتار را توضیح می‌دهد. این همچنین یک نکته استقرار را روشن می‌کند که ارزش دارد صریح گفته شود: تعدادی از viewerهای حداقلی یا غیرسازگار /NeedAppearances را نادیده می‌گیرند و باز هم چیزی نشان نمی‌دهند. برای readerهای رایج، این پرچم کار خودش را می‌کند، اما اگر مخاطبان شما یک renderer توکار غیرمعمول دارند، پیش از هر وعده‌ای آن‌جا تست کنید

افزودن شش نوع فیلد

هر روش همان الگو را دنبال می‌کند. شما شاخص صفحه صفرمبنا، چهار گوشه مستطیل ویجت در مختصات فضای کاربر PDF، نام فیلد، و هر آرگومان اضافه‌ای را که آن نوع لازم دارد می‌دهید. مستطیل X1, Y1, X2, Y2 با مبدأ PDF در گوشه پایین‌چپ صفحه است، بنابراین مقادیر Y بزرگ‌تر بالاتر قرار می‌گیرند؛ این قرارداد مختصات فرمت فایل است، نه قرارداد صفحه‌نمایشِ بالا-چپ، و اشتباه گرفتن آن دومین خطای رایج بعد از فراموش‌کردن پرچم است. هر فراخوانی شاخص صفرمبنا‌ی فیلد جدید را برمی‌گرداند، یا -1 اگر شاخص صفحه خارج از بازه باشد یا شیء صفحه نتواند resolve شود

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

آرگومان‌های سوم و چهارم رشته‌ای فیلد متنی، نام فیلد و مقدار اولیه /V آن هستند؛ عدد صحیح همان /MaxLen است، که فقط وقتی بزرگ‌تر از صفر باشد نوشته می‌شود. HotPDF به هر فیلد قابل‌ویرایش یک رشته‌ی default appearance به صورت /Helv 12 Tf 0 0 0 rg می‌دهد، و این همان چیزی است که یک /NeedAppearances-honoring viewer آن را می‌خواند تا درباره فونت و رنگی که مقدار را با آن رسم می‌کند تصمیم بگیرد. چک‌باکس یک مقدار export می‌گیرد، یعنی رشته‌ای که فرم وقتی box تیک خورده است submit می‌کند، به‌علاوه یک بولی برای حالت اولیه؛ درون‌ساز، ورودی‌های نامی متناظر /V، /AS، و /DV را می‌نویسد تا حالت on/off از لحظه باز شدن فایل یکدست بماند. یک مقدار export خالی به‌طور پیش‌فرض به Yes تبدیل می‌شود، نام on متعارف چک‌باکس

فیلدهای انتخابی و بیت‌های /Ff

ComboBox و ListBox هر دو فیلد انتخابی هستند، با نوع فیلد /Ch در ISO 32000-1 §12.7.4. تفاوت یک dropdown و یک فهرست پیمایشی فقط یک بیت در عدد flags فیلد /Ff: بیت 18، یعنی Combo flag، با مقدار $40000. HotPDF آن بیت را برای AddLoadedComboBox تنظیم می‌کند و برای AddLoadedListBox آن را پاک نگه می‌دارد؛ در غیر این صورت این دو کاملاً یکسان‌اند، و هر دو گزینه‌های خود را به‌صورت یک open array از رشته‌ها می‌گیرند که در ورودی /Opt نوشته می‌شود

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

چند نکته درباره فهرست گزینه‌ها. HotPDF هر ورودی /Opt را به صورت یک رشته ساده می‌نویسد، جایی که مقدار export و برچسب نمایش‌داده‌شده یکی هستند. ISO 32000-1 §12.7.4.4 همچنین فرم دو‌عنصری [export display] را زمانی مجاز می‌داند که بخواهید مقدار ارسال‌شده با چیزی که کاربر می‌خواند فرق داشته باشد؛ روش‌های ایجاد در مسیر بارگذاری‌شده از فرم ساده تک‌رشته‌ای استفاده می‌کنند، پس اگر به export و display متفاوت نیاز دارید باید آن‌ها را خودتان روی دیکشنری حاصل تنظیم کنید. و مقداری که به‌عنوان انتخاب فعلی فیلد می‌دهید باید یکی از گزینه‌هایی باشد که ارائه کرده‌اید، چون viewer آن را با فهرست تطبیق می‌دهد

دکمه فشاری حالت دیگری است که توسط پرچم هدایت می‌شود: نوع فیلد /Btn با بیت 17، یعنی PushButton flag، و مقدار $10000. این بیت همان چیزی است که یک دکمه قابل‌کلیک را از یک چک‌باکس جدا می‌کند، که آن هم یک /Btn field است اما این بیت را ندارد. عنوانی که می‌دهید در دیکشنری ویژگی‌های appearance /MK به‌صورت caption عادی /CA نوشته می‌شود. در این‌جا باید درباره محدوده کار صادق بود: دکمه با برچسب و مستطیلش ساخته می‌شود، اما روش ایجاد در مسیر بارگذاری‌شده هیچ actionی را متصل نمی‌کند، پس به‌تنهایی دکمه‌ای است که درست به نظر می‌رسد و هنگام کلیک هیچ کاری نمی‌کند. وصل‌کردن submit، reset، یا actionهای JavaScript یک موضوع جداست؛ برای سمت authoring از صفر، workflow فیلد به‌اضافه action در ساخت فیلدها و actionهای AcroForm در Delphi پوشش داده شده است، که نقطه مقایسه درست برای چیزی است که مسیر بارگذاری‌شده عمداً کنار می‌گذارد

دیکشنری‌ای که هر فیلد با آن مشترک است

زیرِ هر شش روش یک builder مشترک قرار دارد که annotation ویجت را می‌سازد و آن را در دو جا ثبت می‌کند. این builder /Type /Annot و /Subtype /Widget، /Rect آرایه را از چهار مختصات شما می‌نویسد، و annotation flags /F 4 را که bit چاپ را تنظیم می‌کند تا فیلد علاوه بر صفحه‌نمایش روی کاغذ هم دیده شود، field name /T، field type /FT، flags /Ff، و یک back-reference /P به شیء صفحه را می‌نویسد. سپس فیلد جدید را به آرایه /Fields آکروفرم و به آرایه /Annots همان صفحه اضافه می‌کند، و در مسیر، referenceهای غیرمستقیم را resolve می‌کند تا به‌جای orphan کردن ویجت، آرایه‌های واقعی را گسترش دهد

این ثبت دوگانه مهم است، چون ویجتی که فقط در یکی از این دو فهرست زندگی می‌کند به‌طور ظریفی خراب است. فیلدی که در /Fields وجود دارد اما از /Annots صفحه غایب است، برای فرم شناخته می‌شود اما هرگز رسم نمی‌شود؛ حالت معکوس رسم می‌شود اما برای منطق فرم ناشناخته می‌ماند. HotPDF هر بار که چیزی اضافه می‌کنید هر دو را همگام نگه می‌دارد، و این همان نوع bookkeeping است که در غیر این صورت باید دقیقاً با دست و مطابق spec انجام دهید

چند محدودیت صادقانه

پیش از آن‌که روی این مسیر یک workflow بسازید، انتظارها را درست تنظیم کنید. رفتار flatten-and-regenerate به این بستگی دارد که viewer، /NeedAppearances را رعایت کند، چیزی که Acrobat، موتورهای PDF در مرورگرهای مدرن، و readerهای رایج دسکتاپ را پوشش می‌دهد، اما تضمین سختی برای هر renderer موجود در دنیا نیست. اگر مجبور باشید فایلی تولید کنید که فیلدهایش در همه‌جا یکسان رندر شوند، حتی در viewerهایی که این پرچم را نادیده می‌گیرند، وارد قلمرو appearance-stream می‌شوید و مسیر authoring از صفر که /AP را برای شما رسم می‌کند انتخاب بهتر است. فیلد امضا هم به همین ترتیب، به‌صورت یک widget امضای خالی و آماده امضا ساخته می‌شود؛ قرار دادن فیلد همان امضای رمزنگاری‌شده نیست

برای تغییر دادن آنچه از قبل وجود دارد، نه افزودن به آن، عملیات مرتبط form flattening است، جایی که فیلدهای تعاملی را دوباره در محتوای ثابت صفحه می‌پزید تا مقادیر دائمی و غیرقابل‌ویرایش شوند؛ آن round trip، از جمله این‌که فرم‌های دارای XFA چگونه handled می‌شوند، در تخت‌سازی فیلدهای XFA و AcroForm در Delphi. افزودن فیلدها و تخت‌سازی فیلدها دو سر یک چرخه عمر هستند: این مقاله نشان می‌دهد چگونه interactivity را روی سندی که آن را نداشت سوار کنید، و تخت‌سازی راهی است برای این‌که بعداً وقتی فرم کارش را تمام کرد آن را دوباره بردارید

API فرم سند بارگذاری‌شده که این‌جا نشان داده شده، بخشی از نسخه استاندارد HotPDF Component برای Delphi و C++Builder است، در کنار مرجع کامل flagهای فیلد، مدیریت appearance، و بقیه مدل AcroForm