مقاله فنی

پیوند فیلدهای AcroForm بین PDFها در Delphi با PDFiumPas

جابه‌جا کردن یک بلوک از فیلدهای فرم از قالب سال قبل روی چیدمان امسال همان‌جایی است که رفت‌وبرگشت‌های FDF و XFDF دیگر کافی نیستند: مقدارها می‌رسند، اما استریم‌های ظاهر، اکشن‌های محاسبه و منابع پیش‌فرض نه. PDFiumPas آن حالت را با GraftPdfAcroForm جواب می‌دهد، که کل گراف شیء فیلد را از یک PDF شبیه‌سازی و داخل دیگری می‌نویسد

دلیل اینکه یک خروجی سطح داده نمی‌تواند این کار را بکند ساختاری است. یک فیلد یک رکورد نیست، یک زیرگراف است. ISO 32000-1 §12.7 دیکشنری فرم تعاملی را تعریف می‌کند که /Fields و /CO و /DR و /DA را حمل می‌کند، §12.7.3 دیکشنری‌های فیلد آویخته زیر آن را تعریف می‌کند، و §12.5.6.19 حاشیه‌نویسی‌های ویجت را تعریف می‌کند که به آن فیلدها یک جعبه مرئی روی صفحه می‌دهند. XFDF برگ‌های آن ساختار را حمل می‌کند. پیوند زدن خود ساختار را حمل می‌کند

چرا کپی کردن آرایه /Fields هرگز کافی نیست

کپی کردن /Fields از یک سند به سند دیگر فرمی تولید می‌کند که به هر شکل جالبی شکسته است، چون آرایه فقط ارجاع‌های غیرمستقیم حمل می‌کند و هیچ چیز دیگر. ISO 32000-1 §7.3.10 یک شیء غیرمستقیم را با شماره شیء به‌علاوه نسل آدرس‌پذیر می‌کند، و آن شماره‌ها فقط داخل فایلی که از آن آمده‌اند معنا دارند. آرایه را آن‌طوری پیست کنید و هر ارجاع درونش یا آویزان می‌ماند یا، بدتر، بی‌سروصدا به یک شیء بی‌ربطی حل می‌شود که اتفاقاً آن اسلات را در مقصد اشغال کرده است. زیر هر ارجاع گرافی نشسته که هم اشتراکی است و هم چرخه‌ای. یک دیکشنری فیلد به بچه‌هایش اشاره می‌کند، هر بچه به /Parent خودش برمی‌گردد، یک ویجت به استریم‌های ظاهرش و به صفحه‌ای که از طریق /P حملش می‌کند اشاره می‌کند، استریم‌های ظاهر به فونت‌های دیکشنری منبع پیش‌فرض فرم اشاره می‌کنند، و دیکشنری‌های additional-action زیر /AA به اشیاء بیشتری اشاره می‌کنند. دو ویجت روی صفحات مختلف به‌طور معمول یک فونت و یک XObject ظاهر را به اشتراک می‌گذارند. پس یک پیوند درست باید آن گراف را پیمایش کند، هر شیء قابل‌دسترس را دقیقاً یک بار شبیه‌سازی کند، /P هر ویجت را به صفحه مقصد نگاشت‌شده برگرداند، و ویجت شبیه‌سازی‌شده را به آرایه /Annots آن صفحه اضافه کند — وگرنه فیلد در فرم وجود دارد و روی صفحه نامرئی است. اگر تفاوت بین یک فیلد، ویجتش و حاشیه‌نویسی صفحه‌ای که نمایشش می‌دهد را دنبال کرده‌اید، یادداشت ما درباره ایندکس ویجت در برابر ایندکس حاشیه‌نویسی دقیقاً همان شکاف را پوشش می‌دهد

گراف شیء پشت یک فیلد فرم PDF وقتی PDFiumPas آن را در Delphi پیوند می‌زند: دیکشنری فرم، فیلد، حاشیه‌نویسی‌های ویجت، آرایه‌های حاشیه‌نویسی صفحه مقصد و استریم ظاهر و فونتی که هر دو ویجت به اشتراک می‌گذارند، به‌علاوه ارجاع برگشتی parent که چرخه را می‌بندد
یک فیلد یک زیرگراف چرخه‌ای اشتراکی است، و به همین دلیل است که کپی کردن آرایه /Fields بین اسناد هر ارجاع را آویزان می‌گذارد

GraftPdfAcroForm از شما چه می‌خواهد؟

سه استریم مجزا و یک نگاشت صفحه صریح می‌خواهد. GraftPdfAcroForm مقدارهای Source و Destination و Output را به‌شکل نمونه‌های TStream جدا می‌گیرد، یک آرایه TPdfGraftPageMappings، یک رکورد TPdfAcroFormGraftOptions، یک TPdfCrossDocumentGraftMap اختیاری، و یک TPdfAcroFormGraftReport خروجی. به‌جای پرتاب کردن مقدار Boolean برمی‌گرداند، و در شکست گزارش دلیل را در ErrorMessage حمل می‌کند. نگاشت صفحه در دو طرف یک-مبناست و استنباط نمی‌شود: هر صفحه مبدأ که ویجتی را حمل می‌کند که قصد پیوندش را دارید باید در آن ظاهر شود. پاس دادن nil برای graft map مشروع است — تابع سپس یک نسخه خصوصی را برای طول فراخوانی می‌سازد و آزاد می‌کند — و TPdfAcroFormGraftOptions.Default به شما CollisionPolicy روی pagcpReject می‌دهد، RenamePrefix روی Imported_، MaxObjects برابر 100000، MaxDepth برابر 128 و AllowSignedDestination روی False. آن سه تای آخر بودجه‌اند، و وجودشان به این دلیل است که گراف شیئی که در شرف پیمایشش هستید از فایلی آمده که شما ننوشته‌اید

uses
  Classes, SysUtils, FPdfCompress;

var
  Source, Destination, Output: TMemoryStream;
  Options: TPdfAcroFormGraftOptions;
  Mappings: TPdfGraftPageMappings;
  Report: TPdfAcroFormGraftReport;
begin
  Source := TMemoryStream.Create;
  Destination := TMemoryStream.Create;
  Output := TMemoryStream.Create;
  try
    Source.LoadFromFile('claim-template-2025.pdf');
    Destination.LoadFromFile('claim-layout-2026.pdf');
    Source.Position := 0;
    Destination.Position := 0;

    Options := TPdfAcroFormGraftOptions.Default;

    SetLength(Mappings, 2);
    Mappings[0].SourcePageNumber := 1;
    Mappings[0].DestinationPageNumber := 1;
    Mappings[1].SourcePageNumber := 2;
    Mappings[1].DestinationPageNumber := 3;

    if GraftPdfAcroForm(Source, Destination, Output, Mappings,
      Options, nil, Report) then
      Output.SaveToFile('claim-2026-with-fields.pdf')
    else
      raise Exception.Create(Report.ErrorMessage);
  finally
    Output.Free;
    Destination.Free;
    Source.Free;
  end;
end;

graft map چطور از شبیه‌سازی دوباره یک فونت مشترک جلوگیری می‌کند؟

TPdfCrossDocumentGraftMap یک جدول ارجاع مبدأ-به-مقصد نگه می‌دارد که کلیدهایش هم شماره شیء و هم نسل را حمل می‌کنند، و cloner بازگشتی قبل از فرود به آن مراجعه می‌کند. ترتیب عملیات است که چرخه‌ها را امن می‌کند: cloner اول شماره شیء مقصد را تخصیص می‌دهد و نگاشت را ثبت می‌کند، بعد ارجاع‌های فرزند شیء مبدأ را پیمایش می‌کند. پدری که به بچه‌ای می‌رسد که به parent خودش اشاره می‌کند، parent را از قبل ثبت‌شده می‌یابد و به‌جای بازگشت مجدد، ارجاع مقصد موجود را برمی‌گرداند. همان lookup است که باعث می‌شود یک فونت یا یک استریم ظاهر یا یک اکشن که شش ویجت به اشتراک گذاشته‌اند یک بار شبیه‌سازی و شش بار ارجاع شود. map با یک هش SHA-256 از بایت‌های مبدأ به سند مبدأ مقید می‌شود، که به‌شکل SourceIdentity در معرض است. اگر به GraftPdfAcroForm مپی بدهید که هویتش با مبدأیی که پاس داده‌اید match نشود، فراخوانی را رد می‌کند به‌جای استفاده مجدد از ارجاع‌هایی که هرگز برای این فایل معتبر نبوده‌اند. نگاشت‌های صفحه قبل از شروع شبیه‌سازی داخل همان map بذرگذاری می‌شوند، و این دقیقاً همان چیزی است که باعث می‌شود /P ویجت در نهایت به صفحه مقصد اشاره کند: شیء صفحه مبدأ از قبل به شیء صفحه مقصد نگاشت‌شده حل می‌شود، پس پاس معمولی بازنویسی ارجاع بدون هیچ حالت خاصی آن را هندل می‌کند

graft map بین‌سندی PDFiumPas در Delphi هر ارجاع مبدأ را با شماره شیء و نسل کلید می‌زند، نگاشت مقصد را قبل از فرود ثبت می‌کند تا ارجاع برگشتی parent خاتمه یابد، و مدخل موجود را برمی‌گرداند تا یک فونت مشترک فقط یک بار شبیه‌سازی شود
ثبت نگاشت قبل از پیمایش بچه‌هاست که یک گراف چرخه‌ای را امن می‌کند و یک شیء مشترک را دقیقاً یک بار شبیه‌سازی می‌کند
uses
  Classes, SysUtils, FPdfCompress, FPdfSha256;

var
  GraftMap: TPdfCrossDocumentGraftMap;
  SourceBytes: TBytes;
  EntriesBefore: Integer;
begin
  SetLength(SourceBytes, Source.Size);
  Source.Position := 0;
  if Length(SourceBytes) > 0 then
    Source.ReadBuffer(SourceBytes[0], Length(SourceBytes));

  GraftMap := TPdfCrossDocumentGraftMap.Create(
    AnsiString(SHA256Hex(SHA256Bytes(SourceBytes))));
  try
    EntriesBefore := GraftMap.Count;
    Source.Position := 0;
    if not GraftPdfAcroForm(Source, Destination, Output, Mappings,
      Options, GraftMap, Report) then
    begin
      // مدخل‌های اضافه‌شده توسط این فراخوانی rollback شده‌اند؛
      // هر چیزی که قبل از آن ثبت شده همچنان سالم است.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

همان rollback است که نکته داشتن map به‌صورت شخصی است. PDFiumPas با map فراهم‌شده توسط فراخوان تراکنشی رفتار می‌کند: یک graft ناموفق مدخل‌هایی را که آن فراخوانی اضافه کرد دور می‌ریزد و هر نگاشتی را که از قبل وجود داشت نگه می‌دارد، پس یک رد شدن هرگز کشی از ارجاع‌های اشیائی که هرگز نوشته نشده‌اند به جا نمی‌گذارد. با این حال، برای هر سند مقصد یک map نگه دارید — سمت مقصد هر مدخل یک شماره شیء در آن فایل خاص است، و در فایل دیگری هیچ معنایی ندارد

تداخل نام فیلدها: رد کردن یا تغییر نام

نام‌های فیلد کاملاً واجد شرایط باید داخل یک فرم منحصربه‌فرد بمانند، و PDFiumPas هنگام برخوردشان حدس نمی‌زند منظورتان چه بود. TPdfAcroFormCollisionPolicy دقیقاً دو جواب ارائه می‌کند. زیر pagcpReject یعنی پیش‌فرض، اولین فیلد مبدأ که عنوانش از قبل در مقصد وجود داشته باشد کل graft را با یک خطا قطع می‌کند و استریم خروجی را خالی می‌گذارد. زیر pagcpRename، فیلد مبدأ برخوردی با پیشوند دادن RenamePrefix تغییر نام می‌یابد و graft ادامه می‌یابد، و Report.RenamedFieldCount به شما می‌گوید این چند بار اتفاق افتاد

Options := TPdfAcroFormGraftOptions.Default;
Options.CollisionPolicy := pagcpRename;
Options.RenamePrefix := 'Y2025_';
Options.MaxObjects := 20000;
Options.MaxDepth := 64;

if GraftPdfAcroForm(Source, Destination, Output, Mappings,
  Options, nil, Report) then
begin
  WriteLn('source fields  : ', Report.SourceFieldCount);
  WriteLn('existing fields: ', Report.DestinationFieldCount);
  WriteLn('grafted fields : ', Report.GraftedFieldCount);
  WriteLn('renamed fields : ', Report.RenamedFieldCount);
  WriteLn('cloned objects : ', Report.GraftedObjectCount);
  WriteLn('reused objects : ', Report.ReusedObjectCount);
  WriteLn('mapped pages   : ', Report.MappedPageCount);
  WriteLn('output bytes   : ', Report.OutputByteCount);
end
else
  WriteLn('graft refused  : ', Report.ErrorMessage);

تغییر نام رایگان نیست، و باید عمدی تصمیمش بگیرید نه اینکه برای محو کردن یک خطا سراغش بروید. یک فیلد تغییرنام‌یافته یک فیلد متفاوت است: هر JavaScript در مقصد که با نام خطابش می‌کند، هر مدخل محاسبه در /CO که انسانی علیه نام قدیم نوشته، و هر مصرف‌کننده پایین‌دستی که روی نام فیلد کلید می‌زند باید درباره پیشوند بداند. اگر دو سند واقعاً همان یک فیلد را توصیف می‌کنند، فیکس صادقانه معمولاً آشتی دادن نام‌ها در بالادست است، نه در زمان graft. وقتی graft فرود آمد، پیمایش فرم ادغام‌شده برای تأیید آنچه واقعاً گرفتید گام طبیعی بعدی است، و ناوبری فیلد فرم در PDFiumPas آن پیمایش را پوشش می‌دهد

کجا graft عمداً fail closed می‌شود

هر شرایط مبهم یک خطاست، هرگز یک نتیجه بهترین‌تلاش، و آن یک تصمیم طراحی است که ارزش فهمیدن را دارد قبل از اینکه در پروداکشن غافلگیرتان کند. GraftPdfAcroForm وقتی به هر کدام از این‌ها برخورد کند مقدار False برمی‌گرداند، استریم خروجی را ریست می‌کند و دلیل را گزارش می‌دهد

  • فرم مبدأ یک مدخل /XFA حمل می‌کند — پاکت‌های XFA یک مدل فرم موازی‌اند و نمی‌توانند به دیکشنری‌های فیلد AcroForm تقلیل یابند
  • ویجتی روی صفحه مبدأیی زندگی می‌کند که مدخلی در نگاشت صفحه ندارد، که در غیر این صورت فیلد بی‌سروصدا حذف می‌شد یا به صفحه غلط وصل می‌شد
  • نگاشت‌های صفحه خارج از بازه‌اند، یا دو نگاشت همان صفحه مبدأ یا مقصد را دوباره استفاده می‌کنند
  • هر دو فرم یک دیکشنری منبع پیش‌فرض /DR تعریف می‌کنند، چون ادغام دو فضای نام منبع خطر این را دارد که یک نام موجود را به فونت دیگری باز اشاره کند
  • گراف شیء از MaxObjects فراتر می‌رود یا بازگشت از MaxDepth
  • مقصد یک امضا دارد و AllowSignedDestination برابر False است
  • graft map فراهم‌شده به سند مبدأ دیگری تعلق دارد، یا یک ارجاع مبدأ آویزان است

مسیر نوشتن به همان اندازه محافظه‌کارانه است. PDFiumPas نتیجه را به‌شکل یک بازنگری افزایشی خلوت به مقصد می‌چسباند، بعد خروجی نوشته‌شده را دوباره مادی می‌کند و فرم آن را دوباره می‌خواند: اگر شمارش فیلد نتیجه برابر شمارش فیلد اصلی مقصد به‌علاوه شمارش مبدأ نباشد، کل graft رد می‌شود و خروجی پاک می‌شود. شما هرگز فایلی تا حدی graft‌شده نمی‌گیرید. هزینه آن سیاست واقعی است — یک تداخل /DR یا یک مقصد امضاشده شما را کاملاً متوقف می‌کند، و باید خودتان آن را حل کنید به‌جای پذیرفتن یک تقریب ادغام‌شده — اما جایگزینش فرمی است که به‌خوبی باز می‌شود و غلط محاسبه می‌کند

GraftPdfAcroForm در PDFiumPas چطور در Delphi fail closed می‌شود: بازنگری نوشته‌شده دوباره خوانده و شمارش فیلدش راستی‌آزمایی می‌شود، هر شرایط مبهم مثل XFA یا صفحه نگاشت‌نشده فراخوانی را رد می‌کند، و یک رد شدن فقط مدخل‌های map را که آن فراخوانی اضافه کرد دور می‌ریزد
مسیر نوشتن راستی‌آزمایی‌شده و map تراکنشی دلیل این هستند که یک graft ردشده هرگز فایلی تا حدی ادغام‌شده به جا نمی‌گذارد

کجا grafting ابزار اشتباه است

Grafting ساختار را جابه‌جا می‌کند، پس وقتی از آن استفاده کنید که ساختار چیزی است که کم دارید. اگر هر دو سند از قبل همان مجموعه فیلد را حمل می‌کنند و فقط لازم است مقدارها و حاشیه‌نویسی‌ها را بین‌شان جابه‌جا کنید، مسیر خروجی و ورودی در مقاله داده فرم XFDF سبک‌تر و استاندارد و بازگشت‌پذیر است. وقتی سراغ GraftPdfAcroForm بروید که مقصد اصلاً فیلدی ندارد یا مجموعه متفاوتی دارد، و نیاز دارید ویجت‌ها و استریم‌های ظاهر و اکشن‌ها و ترتیب محاسبه سالم بیایند. یک یادداشت عملی آخر درباره هویت: چون graft map روی شماره شیء به‌علاوه نسل کلید می‌زند و به یک SHA-256 از بایت‌های مبدأ مقید است، دوباره ذخیره کردن یا بهینه کردن مبدأ بین اجراها هویت متفاوتی تولید می‌کند و مپی که دیگر اعمال نمی‌شود. مبدأیی که از آن graft می‌کنید را snapshot کنید و برای بچ پایدار نگهش دارید؛ با آن به‌شکل یک artifact ورودی رفتار کنید، نه چیزی که یک job شبانه آزادانه بازنویسیش کند

GraftPdfAcroForm و TPdfCrossDocumentGraftMap و جعبه‌ابزار PDF سطح استریم اطرافشان همراه PDFiumPas Delphi PDFium Component برای Delphi و C++Builder و Lazarus عرضه می‌شوند، جایی که صفحه محصول مرجع کامل API را برای آپشن‌های graft و فیلدهای گزارش و بقیه سطح ویرایش سند حمل می‌کند