مقاله فنی

فیلدهای PDF چند-انتخابی در رفت‌وبرگشت FDF و XFDF (Delphi)

HotPDF مقدارهای list box چند-انتخابی را در رفت‌وبرگشت از FDF و XFDF با نگه داشتن مقدار فیلد به‌صورت آرایه از سر تا ته جابه‌جا می‌کند. از نسخهٔ 2.755.0 به بعد، ExportLoadedFormToFDF و ExportLoadedInterchangeToFDF و ExportLoadedFormToXFDF هر گزینهٔ انتخاب‌شده را به‌عنوان رشتهٔ FDF یا عنصر <value> مخصوص خودش در XFDF می‌نویسند و متدهای ورود متناظر، هر مقدار را با گزینه‌های فیلد بررسی می‌کنند و شاخص‌های انتخاب /I را قبل از تغییر دادن هر چیزی دوباره می‌سازند. هیچ چیز در طول مسیر به یک رشته چسبانده نمی‌شود

شکستی که این fix می‌کند راحت قابل‌بازتولید است. یک فرم سفارش با یک list box چند-انتخابی از گزینه‌های محصول بگیر، بگذار کاربر دو تا را انتخاب کند، دادهٔ فرم را برای یک سیستم پشتیبانی خروجی بگیر و بعد فایل ویرایش‌شده را دوباره داخل PDF وارد کن. پیش از این تغییر list box خالی یا غلط برمی‌گشت. دلیلش این بود که یکی از مقادیر خروجی حاوی یک شکست خط بود و مسیر قدیمی انتخاب‌ها را در یک رشتهٔ تکی جداشده با خط تخت کرده بود. بیرون کشیدن چند انتخاب از آن رشته هرگز قابل‌اعتماد نبود و با یک مقدار خروجی که خودش شکست خط دارد اصلاً نمی‌شود کارش کرد

چرا چسباندن مقدارهای چند-انتخابی با شکست خط، رفت‌وبرگشت را می‌شکند؟

چسباندن انتخاب‌ها در یک رشته مرزهای بین مقادیر را دور می‌ریزد و یک مقدار می‌تواند حاوی جداکننده باشد، پس هیچ importری نمی‌تواند رشته را درست دوباره بشکافد. ISO 32000-1 §12.7.4.4 اجازه می‌دهد درایهٔ /V یک فیلد انتخاب یا یک رشتهٔ متنی تکی باشد یا آرایه‌ای از رشته‌های متنی، و یک list box با فلگ MultiSelect (بیت 22 در /Ff) به‌محض انتخاب شدن بیش از یک گزینه از شکل آرایه استفاده می‌کند. همان بخش /I را آرایه‌ای از شاخص‌های گزینهٔ صفرمبنا به ترتیب صعودی تعریف می‌کند که viewerها با آن دو گزینه‌ای را که اتفاقاً مقدار export یکسانی دارند از هم تشخیص می‌دهند. در HotPDF، getter اسکالر GetFormFieldValue فقط شکل رشته‌ای را می‌خواند، پس عبور دادن یک آرایه از آن، خروجی را به یک رشتهٔ خالی تنزل می‌داد و ورود قدیمی XFDF عناصر تکراری <value> را با LF می‌چسباند. تصور کن گزینه‌ای که به‌شکل Deep و شکست خط و Blue خروجی گرفته شده: بعد از چسباندن، Deep\nBlue\nRed می‌تواند دو انتخاب باشد یا سه، و فایل هیچ راهی برای فهمیدنش نمی‌دهد. fix این بود که اصلاً از اسکالر در میانهٔ رفت‌وبرگشت استفاده نشود

رفت‌وبرگشت قدیمی چند-انتخابی در HotPDF که در آن دو گزینهٔ انتخاب‌شدهٔ list box، یکی با شکست خط نهفته، توسط مسیر اسکالر GetFormFieldValue در رشتهٔ تکی Deep و شکست خط و Blue و شکست خط و Red تخت می‌شوند که خواننده‌های پایین‌دست می‌توانند آن را یا دو انتخاب یا سه انتخاب parse کنند
چسباندن مقدارهای چند-انتخابی در یک رشته مرزهای مقدار را نابود می‌کند و یک مقدار خروجی که خودش شکست خط دارد شکل تخت‌شده را مبهم می‌کند

فایل‌های FDF و XFDF خروجی چه چیزی در بر دارند؟

HotPDF یک مقدار چند-انتخابی را در FDF به‌شکل آرایهٔ نوع‌دار و در XFDF به‌شکل یک عنصر <value> به‌ازای هر انتخاب می‌نویسد، پس مرزها روی دیسک دیدنی می‌مانند. در FDF هر آیتم املای خودش را در PDF مبدأ نگه می‌دارد: رشته‌های هگزادسیمال به‌صورت hex بیرون می‌روند و رشته‌های literal توسط یک هلپر واحد که CR و LF را به \r و \n تبدیل می‌کند escape می‌شوند. در XFDF ریشهٔ xml:space="preserve" را همان‌طور که ISO 19444-1 می‌خواهد حمل می‌کند، یعنی هر فاصلهٔ سفید داخل یک عنصر متنی داده حساب می‌شود. HotPDF پس تگ شروع و متن escape‌شده و تگ پایان هر <value> را یک‌تکه می‌نویسد، تورفتگی را بیرون از عنصر نگه می‌دارد و CR و LF و TAB را به‌عنوان character reference کدگذاری می‌کند تا یک XML parser که نرمال‌سازی انتهای خط را اعمال می‌کند نتواند بایت‌های اصلی را عوض کند

شکل‌های خروجی که HotPDF از 2.755.0 به بعد برای یک list box چند-انتخابی می‌نویسد: FDF یک آرایهٔ نوع‌دار به‌ازای هر فیلد با /V [(Deep خط تازه Blue) (Red)] و یک مقدار region هگز حمل می‌کند، در حالی که XFDF یک عنصر value به‌ازای هر انتخاب زیر xml:space preserve حمل می‌کند تا فاصلهٔ سفید داده حساب شود
مرزها روی دیسک دیدنی می‌مانند: FDF هر انتخاب را به‌عنوان آیتم آرایهٔ خودش نگه می‌دارد و XFDF هر یک را در یک عنصر value جداگانه می‌نویسد، پس هیچ importری مجبور به حدس زدن نیست
<!-- FDF: یک آرایهٔ نوع‌دار به‌ازای هر فیلد -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: یک <value> به‌ازای هر انتخاب -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

دو حالت مرزی خروجی هست که قبل از نوشتن کد فراخوانی بدانشان بد نیست. اول، ExportLoadedFormToFDF کل بدنهٔ FDF را در حافظه می‌سازد قبل از آنکه فایل مقصد را بسازد (fix شده در 2.755.1)، پس مقداری که خروجی گرفتنش ممکن نیست، مثل آرایه‌ای که چیزی جز رشته دارد، بدون بریدن یک فایل موجود استثنا می‌دهد. دوم، یک انتخاب خالی روی list boxی که همزمان یک مقدار خروجی رشتهٔ خالی هم ارائه می‌دهد در XFDF مبهم است، چون <value/> می‌تواند یعنی هیچ چیز انتخاب نشده یا یعنی گزینهٔ خالی انتخاب شده. ExportLoadedFormToXFDF در آن حالت به‌جای حدس زدن استثنا می‌دهد و قبل از آنکه فایل مقصد باز شود استثنا می‌دهد. FDF چنین ابهامی ندارد، چون /V [] و /V [()] متمایزند. هر دو exporter مربوط به FDF هم پایانه‌های فقط-ویجت را که نام /T ندارند رد می‌کنند، هماهنگ با exporter مربوط به XFDF، چون هیچ importری نمی‌تواند آن درایه‌ها را به فیلدی برگرداند

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // list boxهای چند-انتخابی به‌شکل /V [(...) (...)] نوشته می‌شوند
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // انتخاب خالی به‌علاوهٔ یک گزینهٔ خروجی خالی: XFDF نمی‌تواند
          // آن‌ها را از هم جدا کند و فایل .xfdf موجود دست‌نخورده می‌ماند
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

HotPDF موقع ورود دادن یک مقدار چند-انتخابی چطور اعتبارسنجی می‌کند؟

HotPDF یک آرایهٔ واردشده فقط وقتی می‌پذیرد که هدف یک فیلد انتخاب با فلگ MultiSelect ست باشد و هر مقدار در آرایه با یک مقدار export در آرایهٔ /Opt فیلد بخواند. هر خانهٔ گزینه فقط یک بار می‌تواند مصرف شود، پس فهرستی با دو گزینه که مقدار export مشترک b دارند [<62> <62>] را به‌عنوان دو انتخاب متمایز می‌پذیرد و یک b سوم را رد می‌کند. /I بازسازی‌شده از ترتیب /Opt پیروی می‌کند نه از ترتیب مقادیر ورودی، چون §12.7.4.4 شاخص‌های صعودی می‌خواهد. HotPDF مقدارهای /V و /I تازه را به‌عنوان objectهای جدا ساخته و فقط بعد از قبول شدن همهٔ مقادیر assign می‌کند، پس یک مقدار ردشده هرگز نیم‌آرایه یا شاخص‌های کهنه به جا نمی‌گذارد. کپی روی فیلدی که وارد می‌شود نوشته می‌شود نه داخل آرایهٔ والد مشترک، املای hex رسیده از FDF در ذخیره hex می‌ماند و فیلدهایی که محاسباتشان به list box وابسته است برای محاسبهٔ مجدد علامت می‌خورند. اگر فقط لازم است یک مقدار تکی ست کنی، تنظیم مقدار یک فیلد فرم در یک PDF بارگذاری‌شده از مسیر اسکالر می‌گذرد که به‌عمد چند انتخاب را هندل نمی‌کند

اعتبارسنجی ورود مقادیر چند-انتخابی در HotPDF: هدف باید یک فیلد انتخاب با MultiSelect ست‌شده در /Ff باشد، هر مقدار ورودی باید با یک مقدار خروجی /Opt بخواند و هر خانه فقط یک بار مصرف شود، /I به ترتیب صعودی مطابق /Opt بازسازی می‌شود و /V و /I جدا، فقط بعد از قبول شدن همهٔ مقادیر assign می‌شوند
هر مقدار ورودی قبل از نوشتن هر چیزی با گزینه‌های فیلد بررسی می‌شود، پس یک مقدار ردشده هرگز نیم‌آرایه یا شاخص‌های انتخاب کهنه به جا نمی‌گذارد

بعضی ابزارهای دیگر مقدارهای خروجی ASCII ساده را به‌شکل رشته‌های hex بدون byte order mark می‌نویسند، مثلاً <416272>، و بعد XFDF را با نوشتن همان ارقام hex به‌صورت متن خروجی می‌گیرند. یک مقایسهٔ literal سخت‌گیر در راه برگشت شکست می‌خورد و ورود دادن abort می‌شود. نسخهٔ 2.755.1 یک retry اضافه کرد: وقتی مقداری با هیچ گزینه‌ای نمی‌خواند، HPDFHexSpellingText متن را به‌عنوان payload hex decode می‌کند و نتیجه را دوباره مقایسه می‌کند. retry فقط برای ورودی‌ای اعمال می‌شود که در غیر این صورت خطا می‌داد، پس هرگز مقداری را که از قبل خوانده عوض نمی‌کند. همان انتشار مسیرهای اسکالر و آرایه را از یک decoder یونیکد واحد استفاده کرد که PDFDocEncoding و UTF-16 با هر دو byte order mark و UTF-8 را می‌فهمد. پیش از آن، یک مقدار منطقی می‌توانست در یک مسیر بخواند و در مسیر دیگر شکست بخورد در اسنادی که کدگذاری‌ها را قاطی می‌کردند

چرا یک فایل FDF معتبر همچنان ممکن است موقع parse کردن فیلدهایش گم شود؟

یک scanner مربوط به FDF که رشته‌های هگزادسیمال را دنبال نکند می‌تواند یک dictionary فیلد را نصف کند وقتی یک مقدار hex درست کنار terminator دیکشنری تمام شود. در << /T (region) /V <416273>>> اولین > رشتهٔ hex را می‌بندد، اما یک scanner ساده‌لوح آن را با > بعدی به‌عنوان پایان دیکشنری می‌خواند و فیلد را بی‌سروصدا می‌اندازد. importer مربوط به FDF در سطح فایل از قبل دنبال این بود که داخل رشتهٔ hex هست یا نه، و در 2.755.1 scannerهای آرایه و دیکشنری پشت ImportLoadedInterchangeFromFDF هم همین کار را می‌کنند. مسئلهٔ دوم مربوط به ارجاع‌های غیرمستقیم است. یک فایل FDF یک سند کوچک با نحو PDF با شماره‌گذاری object خودش است (ISO 32000-1 §12.7.7)، پس مقداری مثل /V [11 0 R] به object 11 از فایل FDF اشاره می‌کند، نه به object 11 از PDFی که داری پر می‌کنی. parser ساده‌شدهٔ FDF در HotPDF ارجاع‌های داخل فایل را resolve نمی‌کند، پس چنین آرایه‌ای را رد می‌کند به‌جای آنکه هرچه object 11 اتفاقاً در سند مقصد است بخواند

ورود فایل و stream و XFDF خطاها را متفاوت گزارش می‌کنند

سه مسیر ورود به یک شکل اعتبارسنجی می‌کنند اما شکست‌ها را متفاوت گزارش می‌کنند و ارزش دارد عمداً یکی را انتخاب کنی. ImportLoadedFormFromFDF هر فیلدی که اعتبارسنجی را رد کند رد می‌کند و تعداد فیلدهایی را که واقعاً اعمال کرد برمی‌گرداند، پس یک شمار کمتر از انتظار تنها نشانهٔ مشکل است. ImportLoadedInterchangeFromFDF و ImportLoadedFormFromXFDF روی اولین فیلد ردشده استثنا می‌دهند. هر فیلد مستقلاً ثبت می‌شود، پس فیلدهایی که قبل از استثنا پردازش شدند مقدارهای جدیدشان را نگه می‌دارند. هیچ‌کدام از این‌ها را یک تراکنش روی کل فایل تبادل حساب نکن: اگر رفتار همه-یا-هیچ می‌خواهی، وقتی استثنا رخ داد سند بارگذاری‌شده را دور بریز به‌جای ذخیره کردنش

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // فقط فیلدها؛ مقداری بیرون از /Opt یا هدفی غیر-چند-انتخابی استثنا می‌دهد
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

بزرگ کردن callbackهای XFDF بدون شکستن فراخوانی‌کننده‌های موجود

پشتیبانی آرایه در unit سطح‌پایین‌تر XFDF در یک رکورد جداگانه زندگی می‌کند، یعنی THPDFXFDFArrayAccess، و در overloadهای تازهٔ HPDFXFDFExportFields و HPDFXFDFImportFields، نه در فیلدهای اضافه‌شده به انتهای رکورد موجود THPDFXFDFAccess. دلیلش سازگاری باینری است. کدی که THPDFXFDFAccess را به‌عنوان متغیر محلی پر می‌کند اغلب فقط خانه‌هایی را که می‌شناسد ست می‌کند و بقیه را هرگز پاک نمی‌کند، پس یک pointer تابع تازه که به آن رکورد اضافه می‌شد حاوی زبالهٔ stack می‌شد و کتابخانه آن را callback واقعی می‌گرفت. با یک رکورد جداگانه، فراخوانی‌کننده‌های قدیمی چیدمان و overloadهای قدیمی را نگه می‌دارند و آن overloadها داخلی یک رکورد آرایهٔ همه-nil پاس می‌دهند. overload ورود اسکالر اصلی همچنان برای سازگاری مقادیر تکراری را با LF می‌چسباند و فقط overload آگاه-از-آرایه آن‌ها را جدا نگه می‌دارد. وقتی دیتابیس خودت را وصل می‌کنی، از Default(THPDFXFDFArrayAccess) شروع کن. برای هر فیلد با مقدار فهرستی، از جمله فیلدی که هیچ چیز انتخاب نشده، از GetFormFieldValueArray مقدار True برگردان و برای بازگشت به callback اسکالر False

uses HPDFXFDF;

// pointer تابع ساده، نه "of object": Context دیتابیس خودت را حمل می‌کند
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // اتصال‌های اسکالر موجود تو
  ArrayAccess := Default(THPDFXFDFArrayAccess); // هر خانهٔ استفاده‌نشده nil است
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

تبادل چند-انتخابی روی list boxهایی کار می‌کند که از قبل موجودند و بیت MultiSelect در /Ff شان ست است. برای اینکه فیلدهای انتخاب و بیت‌های فلگشان در وهلهٔ اول چطور ساخته می‌شوند، افزودن ListBox و بقیهٔ فیلدهای AcroForm به یک PDF بارگذاری‌شده را ببین. برای markupهای درج‌تعلیق که از درخت <annots> مربوط به XFDF می‌گذرند، ورود و خروج XFDF برای annotationها در HotPDF را ببین. مرجع کامل API و دانلود آزمایشی در صفحهٔ کامپوننت PDF در Delphi از HotPDF است