جابهجا کردن یک بلوک از فیلدهای فرم از قالب سال قبل روی چیدمان امسال همانجایی است که رفتوبرگشتهای 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 آن صفحه اضافه کند — وگرنه فیلد در فرم وجود دارد و روی صفحه نامرئی است. اگر تفاوت بین یک فیلد، ویجتش و حاشیهنویسی صفحهای که نمایشش میدهد را دنبال کردهاید، یادداشت ما درباره ایندکس ویجت در برابر ایندکس حاشیهنویسی دقیقاً همان شکاف را پوشش میدهد
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 ویجت در نهایت به صفحه مقصد اشاره کند: شیء صفحه مبدأ از قبل به شیء صفحه مقصد نگاشتشده حل میشود، پس پاس معمولی بازنویسی ارجاع بدون هیچ حالت خاصی آن را هندل میکند
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 یا یک مقصد امضاشده شما را کاملاً متوقف میکند، و باید خودتان آن را حل کنید بهجای پذیرفتن یک تقریب ادغامشده — اما جایگزینش فرمی است که بهخوبی باز میشود و غلط محاسبه میکند
کجا 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 و فیلدهای گزارش و بقیه سطح ویرایش سند حمل میکند