مقاله فنی

تنظیم مقدار فیلدهای فرم در یک PDF بارگذاری‌شده با Delphi

HotPDF Delphi Component یک فیلد AcroForm موجود در یک PDF بارگذاری‌شده را از طریق THotPDF.SetFormFieldValue پر می‌کند، چه با آدرس دادن بر اساس شاخص صفرمبنای فیلد و چه با نام کامل واجدشرایط فیلد. نوشتن درایهٔ جدید /V بخش آسان ماجراست؛ آنچه این فراخوانی را روی فرم‌های دنیای واقعی قابل‌اعتماد می‌کند این است که همان متد سه قطعه حالت را هم سازگار نگه می‌دارد که تا خراب نشوند نامرئی‌اند: هویت decode‌شدهٔ فیلد تا اصلاً بتوان نامی غیر-ASCII را پیدا کرد، وضعیت ظاهری /AS روی widgetهای چک‌باکس و رادیو، و آرایهٔ شاخص انتخاب /I روی فیلدهای انتخاب. خود appearance stream قابل‌مشاهده یک مرحلهٔ جداگانه و صریح از طریق EnsureLoadedFieldAppearanceStream است

سناریو همان سناریوی روزمره است: مشتری فرم خودش را برایت می‌فرستد، یک اظهارنامهٔ مالیاتی، یک claim بیمه، یک سفارش خرید که کسی سال‌ها پیش در Acrobat ساخته، و اپلیکیشن Delphi تو باید آن را از یک دیتابیس پر کند و فایلی برگرداند که همه‌جا درست باز شود. روی اینکه فرم چطور authoring شده هیچ کنترلی نداری. نام فیلدها ممکن است با UTF-16 کدگذاری شده باشند، مقدارهای export چک‌باکس ممکن است 2 باشند نه Yes، و کمبوباکس‌ها ممکن است از جفت‌های گزینهٔ [export display] استفاده کنند. هر یک از این جزئیات قاعده‌ای در ISO 32000-1 دارد و هر قاعده چیزی است که SetFormFieldValue حالا برایت مدیریت می‌کند. این مقاله دربارهٔ آن است که چه می‌کند، چرا، و کجا می‌ایستد. برای مسئلهٔ خواهرش، یعنی ساختن فیلدهایی که هنوز وجود ندارند، افزودن فیلدهای AcroForm به یک PDF بارگذاری‌شده در Delphi را ببین

چرا SetFormFieldValue فیلدی با نام غیر-ASCII را پیدا نمی‌کند؟

پیش از v2.752.1 پاسخ کدگذاری بود: فیلد زیر یک نام UTF-16BE هگزادسیمالی در فایل زندگی می‌کرد و cache نام به‌جای متن، املای hex را ذخیره می‌کرد. ISO 32000-1 §12.7.3.1 نام جزئی فیلد /T را به‌عنوان یک text string تعریف می‌کند و §7.9.2.2 می‌گوید یک text string می‌تواند UTF-16BE با یک byte order mark FE FF در ابتدا باشد. ابزارهای authoring به‌طور روتین چنین نام‌هایی را طبق §7.3.4.3 به‌صورت رشته‌های hex سریالایز می‌کنند، پس فیلدی به نام Straße به شکل <FEFF005300740072006100DF0065> می‌رسد. داخل HotPDF، فیلد THPDFStringObject.Value هر وقت IsHexadecimal ست باشد متن hex خام را نگه می‌دارد، که دقیقاً همان چیزی است که برای یک رفت‌وبرگشت بی‌اتلاف از dictionary اصلی می‌خواهی و دقیقاً همان چیزی که به‌عنوان کلید جستجو نمی‌خواهی. HPDFLoadedFormTextName این دو دغدغه را از هم جدا می‌کند. وقتی cache رابطه ساخته می‌شود، هر مقدار /T از آن می‌گذرد: اگر شیء رشته هگزادسیمال باشد HPDFHexToBytes توالی byte را برمی‌گرداند؛ اگر byteها با FE FF شروع شوند و طول زوج داشته باشند، payload به‌عنوان UTF-16BE decode و به‌صورت UTF-8 دوباره کدگذاری می‌شود؛ بعد نتیجه با یک نقطه به نام والدش می‌چسبد تا نام کامل واجدشرایطی بسازد که §12.7.3.1 توصیف می‌کند، پس فرزندی به نام City زیر والدی به نام Address به‌صورت Address.City ثبت می‌شود. کلید cache به حروف کوچک نرمال می‌شود و همین باعث می‌شود SetFormFieldValue('address.city', ...) هم کار کند؛ این یک راحتی فراتر از استاندارد است، چون spec با نام‌ها به‌صورت حساس به حروف رفتار می‌کند. نکتهٔ حیاتی این است که فقط کلید cache عوض می‌شود. شیء /T در dictionary فیلد کدگذاری هگزادسیمالی‌اش را نگه می‌دارد، پس ذخیره کردن سند هویت فیلدی را که فقط پرش کرده‌ای بازنویسی نمی‌کند

نحوهٔ resolve کردن نام‌های غیر-ASCII در AcroForm توسط HotPDF: HPDFHexToBytes payload UTF-16BE پشت یک رشتهٔ /T هگزادسیمالی را برمی‌گرداند، byte order mark یعنی FE FF decode و به‌صورت UTF-8 دوباره کدگذاری می‌شود، و نام واجدشرایط به والدش می‌پیوندد تا هم Applicant.FullName و هم فیلدی به نام Straße در cache جستجو بنشینند
فقط کلید cache عوض می‌شود: dictionary فیلد کدگذاری هگزادسیمالی‌اش را نگه می‌دارد، جستجوها به‌عنوان راحتی فراتر از استاندارد به حروف کوچک نرمال می‌شوند و ذخیرهٔ سند هرگز هویت فیلدی را که فقط پرش کرده‌ای بازنویسی نمی‌کند
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // نام‌های واجدشرایط از رشته‌های UTF-16BE مربوط به /T decode و
    // با نقطه به هم می‌چسبند، پس نام‌های تودرتو و غیر-ASCII resolve می‌شوند
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // مقدارهایی که Latin-1 نیستند به‌صورت hex UTF-16BE با پیشوند FEFF
    // سفر می‌کنند و به‌عنوان یک رشتهٔ هگزادسیمالی PDF نوشته می‌شوند
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

SetFormFieldValue واقعاً چه چیزی می‌نویسد؟

هر دو overload همان پنج مرحله را اجرا می‌کنند: پیدا کردن dictionary فیلد، نوشتن /V از طریق HPDFSetDictFormValue، سازگار کردن شاخص‌های انتخاب در فیلد انتخاب، کثیف علامت زدن dictionary، سازگار کردن وضعیت‌های ظاهری دکمه، و در آخر ثبت شاخص فیلد از طریق NoteLoadedFormFieldDirty. آن مرحلهٔ آخر اگر فرم اسکریپت‌های محاسبه داشته باشد مهم است، چون مجموعهٔ کثیف همان چیزی است که overload بدون-آرگومان RecalculateLoadedFormFieldsIncremental مصرف می‌کند تا فقط محاسباتی را دوباره اجرا کند که به‌صورت گذرا یک فیلد تغییرکرده را می‌خوانند. خود HPDFSetDictFormValue دربارهٔ نوع شیئی که جایگزین می‌کند دقیق است. اگر /V موجود یک شیء name باشد — که چک‌باکس و فیلدهای رادیو برای مقدار export خود از آن استفاده می‌کنند — مقدار جدید به‌صورت یک name نوشته می‌شود، هرگز به‌صورت یک string، چون نام‌های PDF بنا به ساختار فقط ASCII هستند. در غیر این صورت یک شیء string می‌نویسد و مقداری که پاس داده‌ای را بازرسی می‌کند: رشته‌ای که با FEFF شروع می‌شود، طول زوج دارد و فقط از ارقام hex تشکیل شده، به‌عنوان شکل انتقالی UTF-16BE از §7.9.2.2 در نظر گرفته می‌شود و با IsHexadecimal ست ذخیره می‌شود، پس به‌صورت <FEFF...> سریالایز می‌شود نه به‌صورت یک (FEFF...) تحت‌اللفظی. همین سازوکار همان چیزی است که خط مربوط به City در بالا به آن تکیه دارد؛ هر رشتهٔ دیگری به‌عنوان یک string تحت‌اللفظی با همان byteهایی که دادی ذخیره می‌شود، پس برای متن لاتین ساده، متن ساده پاس بده

چرا یک چک‌باکس بعد از تغییر مقدار باز هم تیک قدیمی‌اش را نگه می‌دارد؟

چون برای یک فیلد دکمه، فقط مقدار تصمیم نمی‌گیرد که چه چیزی رسم شود. ISO 32000-1 §12.7.4.2.3 تعیین می‌کند که یک widget چک‌باکس وضعیت ظاهری /AS را حمل می‌کند که نام می‌برد کدام stream در /AP /N در آن لحظه نمایش داده می‌شود، و viewerها از /AS رسم می‌کنند، نه از /V. اگر /V را به Yes عوض کنی اما /AS را روی Off رها کنی، فایل از داخل متناقض است و flatten کردن با کمال میل همان ظاهر کهنهٔ تیک‌نخورده را در صفحه می‌پزد در حالی که دادهٔ فرم می‌گوید تیک‌خورده. ReconcileLoadedButtonAppearanceStates وجود دارد تا همین شکاف را ببندد: برای فیلدی که /FT اش Btn است، هم خود dictionary فیلد را می‌بیند و هم هر درایهٔ آرایهٔ /Kids اش را، نام حالت روشن را از /AP /N می‌خواند و /AS را وقتی با مقدار فیلد می‌خواند به همان نام و وقتی نمی‌خواند به Off بازنویسی می‌کند

چرا یک چک‌باکس HotPDF وقتی فقط /V عوض می‌شود تیک قدیمی‌اش را نگه می‌دارد: viewerها از وضعیت ظاهری /AS در /AP /N رسم می‌کنند، پس ReconcileLoadedButtonAppearanceStates به فیلد و هر فرزندش سر می‌زند، نام حالت روشن را به‌عنوان اولین کلید غیر از Off می‌خواند و /AS را در صورت تطابق یا در غیر آن به Off بازنویسی می‌کند
گروه‌های رادیویی هر فرزند را با مقدار والد مقایسه می‌کنند که InheritedButtonValue با پیمودن زنجیرهٔ /Parent بازیابی می‌کند، پس ست کردن گروه روی یک مقدار export دقیقاً همان widget را روشن و هر خواهر و برادرش را خاموش می‌کند

دو جزئیات از فرم‌های واقعی شکل fix مربوط به v2.752.3 را ساختند. اول، یک dictionary ظاهر عادی مجاز است فقط حالت روشن را داشته باشد؛ §12.7.4.2.3 ظاهر خاموش را Off نام می‌برد اما ابزارهای authoring مرتباً stream آن را جا می‌اندازند و می‌گذارند viewer هیچ چیزی رسم کند. کد قبلی وقتی dictionary کمتر از دو درایه داشت یکسره بیرون می‌رفت، پس آن چک‌باکس‌های تک-حالت بی‌صدا تیک قدیمی‌شان را نگه می‌داشتند. بررسی حالا به‌سادگی این است که dictionary غیرتهی باشد و نام حالت روشن همان اولین کلیدی گرفته می‌شود که Off نیست. دوم، نام حالت روشن هر چه نویسنده انتخاب کرده باشد. فرم‌های واقعی از 2 یا Yes یا On یا یک واژهٔ محلی‌شده استفاده می‌کنند، پس مقایسه با خود کلید است، بی‌توجه به بزرگی و کوچکی حروف، و هرگز با یک Yes ثابت‌شده. دکمه‌های رادیویی یک پیچ دیگر هم اضافه می‌کنند که در §12.7.4.2.4 توصیف شده: انتخاب در /V روی فیلد والد می‌نشیند، در حالی که widgetها مالِ فرزندان جداگانه‌اند و معمولاً هیچ /V خودشان ندارند. پس هلپر تودرتوی InheritedButtonValue زنجیرهٔ /Parent را تا 64 سطح بالا می‌رود تا یک مقدار غیرتهی پیدا کند، و هر فرزند با مقدار گروهی که به آن تعلق دارد مقایسه می‌شود. ست کردن والد روی مقدار export یک فرزند، دقیقاً همان فرزند را روشن و هر خواهر و برادرش را خاموش می‌کند

// چک‌باکس: مقدار export باید با کلید حالت روشن در /AP /N بخواند
// (اغلب 'Yes'، اما فرم‌های واقعی از '2' یا 'On' یا هر چیز دیگری استفاده می‌کنند)
Pdf.SetFormFieldValue('Consent', 'Yes');

// گروه رادیو: /V روی والد نوشته می‌شود؛ هر widget فرزند
// /AS را روی نام export خودش یا Off می‌گیرد
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// پاک کردن یک چک‌باکس: هر مقداری که با هیچ حالت روشنی نخواند /AS را Off می‌کند
Pdf.SetFormFieldValue('Newsletter', 'Off');

فیلدهای انتخاب: هم‌قدم نگه داشتن /I با /V

برای یک کمبوباکس یا list box، /V تنها جایی نیست که یک انتخاب ثبت می‌شود. جدول 231 در §12.7.4.4 فیلد /I را به‌عنوان آرایه‌ای از شاخص‌های صفرمبنا در /Opt تعریف می‌کند که آیتم‌های انتخاب‌شده را مشخص می‌کند، و viewerی که ببیند /I به گزینهٔ 0 اشاره می‌کند در حالی که /V گزینهٔ 3 را نام می‌برد ممکن است سطر اشتباه را هایلایت کند. از v2.754.1 به بعد HPDFReconcileChoiceSelection داخل هر فراخوانی SetFormFieldValue اجرا می‌شود و وقتی /FT ارثی برابر Ch باشد /I را از مقدار جدید بازمی‌سازد. ترتیب عملیات عمدی است. درایهٔ محلی /I اول حذف می‌شود، بی‌آنکه به محتوایش دست بخورد: اگر آرایهٔ قدیمی یک شیء غیرمستقیم بود که با فیلد دیگری شریک بود، تغییر دادنش درجا انتخاب آن فیلد دیگر را خراب می‌کرد، پس روتیین ارجاع را می‌اندازد و یک آرایهٔ مستقیم تازه می‌سازد. بعد /Opt را از طریق زنجیرهٔ /Parent resolve می‌کند، چون گزینه‌های فیلد انتخاب می‌توانند ارثی باشند، و درایه‌ها را اسکن می‌کند. یک گزینهٔ رشتهٔ ساده مستقیم مقایسه می‌شود؛ یک جفت [export display] روی عنصر export آن مقایسه می‌شود و جفتی با کمتر از دو عنصر رد می‌شود. هر دو طرف از HPDFLoadedFormTextName می‌گذرند، پس یک گزینهٔ hex UTF-16 با یک مقدار hex UTF-16 می‌خواند بی‌آنکه تو آن‌ها را یکسان بنویسی. در اولین تطابق یک /I تک-عنصری نوشته می‌شود و اسکن می‌ایستد؛ یک مقدار اسکالر همیشه هر انتخاب چندگانهٔ قبلی را جایگزین می‌کند، بی‌توجه به پرچم MultiSelect

نحوهٔ سازگار نگه داشتن یک فیلد انتخاب توسط HotPDF: HPDFReconcileChoiceSelection آرایهٔ محلی /I را پیش از دست زدن به آن حذف می‌کند، /Opt را از طریق زنجیرهٔ /Parent resolve می‌کند، نیمهٔ export هر گزینه را از HPDFLoadedFormTextName می‌گذراند، در اولین تطابق یک /I تک-عنصری می‌نویسد و وقتی مقدار یک کمبوی قابل‌ویرایش هیچ شاخصی ندارد هیچ چیزی نمی‌نویسد
یک گزینهٔ رشتهٔ ساده مستقیم مقایسه می‌شود و یک جفت export display روی عنصر exportش، در حالی که مقداری بیرون از /Opt به‌درستی هیچ شاخصی جا نمی‌گذارد — یک /I کهنهٔ اشاره‌کننده به سطر اشتباه از هیچ شاخصی بدتر می‌بود

وقتی هیچ چیزی منطبق نشود، اصلاً هیچ /I ای نوشته نمی‌شود. این نتیجهٔ درست برای یک کمبوباکس قابل‌ویرایش است، جایی که §12.7.4.4 اجازه می‌دهد کاربر مقداری بیرون از فهرست گزینه‌ها تایپ کند؛ چنین مقداری شاخصی ندارد و یک شاخص کهنه از هیچ شاخصی بدتر می‌بود. همین را هم می‌گیری اگر به یک فهرست گزینهٔ جفتی به‌جای مقدار export یک برچسب نمایش بدهی، پس وقتی یک کمبوباکس از نشان دادن انتخاب تو سرباز می‌زند، ببین کدام نیمهٔ جفت را داده‌ای

// /Opt برابر است با [[US United States] [CA Canada] [MX Mexico]]:
// روی مقدار export تطابق کن، و /I می‌شود [1]
Pdf.SetFormFieldValue('Country', 'CA');

// کمبوی قابل‌ویرایش با مقداری بیرون از /Opt: /V نوشته می‌شود،
// /I حذف می‌شود و هیچ شاخصی جعل نمی‌گردد
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

مقدار و ظاهر دو عملیات جداگانه‌اند

SetFormFieldValue هرگز به appearance stream یک فیلد متنی یا انتخاب دست نمی‌زند. بعد از این فراخوانی، /V متن جدید را نگه می‌دارد در حالی که /AP /N باز هم متن قدیمی را رسم می‌کند، و اینکه viewer کدام را نشان دهد بستگی دارد به اینکه dictionary مربوط به AcroForm طبق §12.7.3.3 پرچم /NeedAppearances true را حمل کند و به اینکه viewer محترمش بشمارد. اگر لازم داری فایل در هر readerی مقدار جدید را رندر کند، از جمله flattenerها و تولیدکنندگان thumbnail که پرچم را نادیده می‌گیرند، EnsureLoadedFieldAppearanceStream را با شاخص فیلد صدا بزن. این متد یک Form XObject از رشتهٔ ارثی /DA و quadding مربوط به /Q و چیدمان شانه‌ای /MaxLen و مقدار می‌سازد، فونت نام‌دار را از طریق منابع /DR در AcroForm resolve می‌کند تا یک فونت Type0 فونت فرزند خودش را نگه دارد نه اینکه به Helvetica تنزل کند، و وقتی دست‌کم یک widget streamی گرفته باشد True برمی‌گرداند. overload نام‌دار SetFormFieldValue هیچ شاخصی به تو برنمی‌گرداند، پس یکی را از طریق GetFormField بگیر که یک THPDFLoadedFormField برمی‌گرداند که مالِ خودت است و باید آزادش کنی. سویت رگرسیون تغییر v2.752.1 دربارهٔ همین تفکیک صریح است: یک مقدار ست می‌کند، EnsureLoadedFieldAppearanceStream را صدا می‌زند، بعد صفحه را رندر می‌کند و بررسی می‌کند که پیکسل‌های داخل مستطیل widget عوض شده‌اند و پیکسل‌های بیرونش نه. تأیید اینکه /V عوض شده هیچ چیزی دربارهٔ آنچه کاربر خواهد دید اثبات نمی‌کند

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // مقدار جدید را در /AP بریز تا viewerهایی که
    // /NeedAppearances را نادیده می‌گیرند باز هم نشانش دهند
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

محدودیت‌هایی که پیش از ساختن روی این پایه باید بدانی

ReconcileLoadedButtonAppearanceStates فیلد /FT محلی همان dictionaryی را می‌آزماید که آدرسش داده‌ای، پس روی والد رادیو یا روی چک‌باکسی که /FT خودش را دارد عمل می‌کند؛ یک widget فرزند که خودش آدرس داده شده و /FT فقط روی والدش است از آن مسیر سازگار نمی‌شود. HPDFReconcileChoiceSelection یک مقدار اسکالر واحد را مدیریت می‌کند و حداکثر یک شاخص می‌نویسد؛ list boxهای چند-انتخابی با چند درایهٔ برگزیده بیرون از چیزی‌اند که SetFormFieldValue مدل می‌کند. هیچ‌یک از این دو روتیین مقداری که پاس می‌دهی را با /Opt یا با کلیدهای حالت روشن اعتبارسنجی نمی‌کند، پس یک غلط تایپی به‌جای یک استثنا، یک چک‌باکس Off یا یک کمبوی بدون شاخص تولید می‌کند. و GetFormFieldValue متن ذخیره‌شدهٔ /V را همان‌طور که در dictionary نشسته برمی‌گرداند، که برای یک مقدار هگزادسیمالی یعنی املای hex، نه متن decode‌شده

وقتی مقدارها داخل آمدند و ظاهرها رسم شدند، دو قدم طبیعی بعدی در دو طرف این عملیات می‌نشینند. تبادل انبوه دادهٔ فیلد با سیستم‌های بیرونی، به‌جای یک SetFormFieldValue در هر بار، همان چیزی است که ورود و خروج XFDF در Delphi پوشش می‌دهد. و وقتی فرم پرشده نهایی شد و دیگر نباید قابل‌ویرایش باشد، تخت‌سازی فیلدهای AcroForm و XFA در Delphi دقیقاً همان وضعیت‌های /AS و appearance streamهایی را که اینجا توصیف شد در محتوای ثابت صفحه می‌پزد، و همین است که سازگار کردنشان پیش از تخت‌سازی اختیاری نیست

API ویرایش فرم بارگذاری‌شده در این مقاله، از جمله SetFormFieldValue و EnsureLoadedFieldAppearanceStream و گراف محاسبهٔ دوبارهٔ افزایشی، بخشی از HotPDF Delphi Component برای Delphi و C++Builder است