مقال تقني

ترحيل حقول 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، وتيارات المظهر تشير إلى الخطوط في قاموس الموارد الافتراضية للنموذج، وقواميس الإجراءات الإضافية تحت /AA تشير إلى المزيد من الكائنات. تشترك ودجتان على صفحتين مختلفين في العادة في خط واحد وكائن XObject مظهر واحد. لذا فإن الترحيل الصحيح يجب أن يجتاز ذلك المخطط، ويستنسخ كل كائن يمكن بلوغه مرة واحدة بالضبط، ويعيد توجيه /P لكل ودجة إلى صفحة الوجهة المعيّنة، ويضيف الودجة المستنسخة إلى مصفوفة /Annots لتلك الصفحة — وإلا فإن الحقل يوجد في النموذج لكنه غير مرئي على الصفحة. إذا لاحقت الفرق بين الحقل وودجته والتعليق التوضيحي للصفحة الذي يعرضه، فإن ملاحظتنا حول فهرس الودجات مقابل فهرس التعليقات التوضيحية تغطي ذلك الانقسام بالضبط

مخطط الكائنات خلف حقل نموذج PDF واحد بينما يرحّله PDFiumPas في Delphi: قاموس النموذج، والحقل، وتعليقات الودجات التوضيحية، ومصفوفات تعليقات صفحة الوجهة، وتيار المظهر والخط اللذين تشترك فيهما الودجتان، بالإضافة إلى مرجع الأصل العائد الذي يُغلق الدورة
الحقل مخطط فرعي مشترك ودائري، ولهذا يترك نسخ مصفوفة /Fields عبر المستندات كل مرجع معلّقًا بلا هدف

ماذا يحتاج GraftPdfAcroForm منك؟

يحتاج إلى ثلاثة تيارات منفصلة وتعيين صفحات صريح. تأخذ GraftPdfAcroForm كلاً من Source وDestination وOutput كنسخ TStream منفصلة، ومصفوفة TPdfGraftPageMappings، وسجل TPdfAcroFormGraftOptions، وTPdfCrossDocumentGraftMap اختياريًا، وTPdfAcroFormGraftReport كمُخرَج. تُرجع الدالة Boolean بدلًا من إطلاق استثناء، وعند الفشل يحمل التقرير السبب في ErrorMessage. تعيين الصفحات يبدأ من الواحد في الطرفين ولا يُستنتج: كل صفحة مصدر تحمل ودجة تنوي ترحيلها يجب أن تظهر فيه. تمرير nil بدلًا من خريطة الترحيل أمر مشروع — تنشئ الدالة عندها خريطة خاصة وتحررها طوال مدة الاستدعاء — و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;

كيف تتجنب خريطة الترحيل استنساخ خط مشترك مرتين؟

تحتفظ TPdfCrossDocumentGraftMap بجدول مراجع من المصدر إلى الوجهة تحمل مفاتيحه رقم الكائن ورقم الجيل معًا، ويستشيره المستنسِخ التكراري قبل أن يهبط إلى المستوى الأدنى. ترتيب العمليات هو ما يجعل الدورات آمنة: يخصّص المستنسِخ رقم كائن الوجهة ويسجّل التعيين أولًا، ثم يجتاز مراجع العناصر الفرعية لكائن المصدر. الأصل الذي يصل إلى عنصر فرعي يشير عائدًا إلى أصله يجد الأصل مسجّلًا بالفعل فيُرجع مرجع الوجهة الموجود بدلًا من التكرار. عملية البحث نفسها هي ما يجعل الخط أو تيار المظهر أو الإجراء المشترك بين ست ودجات يُستنسخ مرة واحدة ويُشار إليه ست مرات. تُربط الخريطة بمستند المصدر عبر بصمة SHA-256 لبايتات المصدر، مكشوفة بوصفها SourceIdentity. إذا سلّمت GraftPdfAcroForm خريطة لا تطابق هويتها المصدر الذي مررته، ترفض الدالة الاستدعاء بدلًا من إعادة استخدام مراجع لم تكن صالحة لهذا الملف قط. تُبذر تعيينات الصفحات في الخريطة نفسها قبل بدء الاستنساخ، وهذه هي بالضبط الطريقة التي ينتهي بها /P الخاص بالودجة مشيرًا إلى صفحة الوجهة: كائن صفحة المصدر يُحلّ أصلًا إلى كائن صفحة الوجهة المعيّنة، لذا تتولى تمريرة إعادة كتابة المراجع العادية أمره دون أي حالة خاصة

خريطة ترحيل PDFiumPas عبر المستندات في Delphi تُرمّز كل مرجع مصدر برقم الكائن ورقم الجيل، وتسجّل تعيين الوجهة قبل الهبوط بحيث ينتهي مرجع الأصل العائد، وتُرجع المدخل الموجود بحيث يُستنسخ الخط المشترك مرة واحدة فقط
تسجيل التعيين قبل اجتياز العناصر الفرعية هو ما يجعل المخطط الدائري آمنًا والكائن المشترك يُستنسخ مرة واحدة بالضبط
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
      // تم التراجع عن المداخل التي أضافها هذا الاستدعاء؛
      // كل ما سُجّل قبله لا يزال سليمًا.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

ذلك التراجع هو بيت القصيد من امتلاك الخريطة بنفسك. يتعامل PDFiumPas مع الخريطة التي يوفرها المستدعي تعاملًا معاملاتيًا: الترحيل الفاشل يتجاهل المداخل التي أضافها ذلك الاستدعاء ويبقي كل تعيين كان موجودًا قبله، لذا فإن الرفض الواحد لا يخلّف أبدًا ذاكرة تخزين مؤقت لمراجع كائنات لم تُكتب قط. احتفظ بخريطة واحدة لكل مستند وجهة — فالجانب المقابل للوجهة في كل مدخل هو رقم كائن في ذلك الملف تحديدًا، ولا يعني شيئًا في ملف مختلف

تعارضات أسماء الحقول: الرفض أو إعادة التسمية

يجب أن تظل أسماء الحقول المؤهلة بالكامل فريدة داخل النموذج، ولن يخمّن PDFiumPas قصدك عند تعارضها. يقدّم TPdfAcroFormCollisionPolicy إجابتين اثنتين بالضبط. في ظل pagcpReject، وهو الافتراضي، أول حقل مصدر يوجد عنوانه بالفعل في الوجهة يُجهض الترحيل كله بخطأ ويترك تيار الإخراج فارغًا. في ظل pagcpRename، تُعاد تسمية حقل المصدر المتعارض بإضافة بادئة RenamePrefix ويستمر الترحيل، ويخبرك 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 كتبه إنسان بالاسم القديم، وأي مستهلك لاحق يعتمد على اسم الحقل، سيحتاج جميعه إلى معرفة البادئة. إذا كان المستندان يصفان فعلًا الحقل نفسه، فإن الإصلاح الأمين عادة هو توفيق الأسماء في المنبع، لا وقت الترحيل. بعد أن ينجح الترحيل، يكون اجتياز النموذج المدموج للتأكد مما حصلت عليه فعلًا هو الخطوة التالية الطبيعية، والتنقل بين حقول النماذج في PDFiumPas يغطي ذلك الاجتياز

حيث يفشل الترحيل بإحكام عمدًا

كل شرط ملتبس هو خطأ، وليس نتيجة مبذولة بأفضل جهد، وهذا قرار تصميمي يستحق الفهم قبل أن يفاجئك في الإنتاج. تُرجع GraftPdfAcroForm القيمة False، وتعيد ضبط تيار الإخراج وتُبلّغ عن السبب عند مصادفتها أيًا مما يلي

  • يحمل نموذج المصدر مدخل /XFA — حزم XFA هي نموذج نماذج موازٍ ولا يمكن اختزالها إلى قواميس حقول AcroForm
  • توجد ودجة على صفحة مصدر ليس لها مدخل في تعيين الصفحات، وهو ما كان سيُسقط الحقل بصمت أو يربطه بالصفحة الخطأ لولا ذلك
  • تعيينات الصفحات خارج النطاق، أو تعيينان يعيدان استخدام صفحة المصدر أو الوجهة نفسها
  • يُعرّف النموذجان كلاهما قاموس موارد افتراضية /DR، لأن دمج فضائي أسماء موارد قد يخاطر بإعادة توجيه اسم موجود إلى خط مختلف
  • مخطط الكائنات يتجاوز MaxObjects أو التكرار يتجاوز MaxDepth
  • الوجهة تحتوي على توقيع وAllowSignedDestination مضبوط على False
  • خريطة الترحيل الممرّرة تخص مستند مصدر مختلفًا، أو أحد مراجع المصدر معلّق بلا هدف

مسار الكتابة متحفّظ بالقدر نفسه. يُصدر PDFiumPas النتيجة بوصفها مراجعة تزايدية متفرقة تُلحَق بالوجهة، ثم يعيد تجسيد الناتج المكتوب ويعيد قراءة نموذجه: إذا لم يساوِ عدد حقول النتيجة عدد حقول الوجهة الأصلي مضافًا إليه حقول المصدر، يُرفض الترحيل كله ويُمسح الإخراج. لن تحصل أبدًا على ملف مُرحَّل جزئيًا. ثمن تلك السياسة حقيقي — تعارض /DR أو الوجهة الموقّعة يوقفك تمامًا، وعليك حلّه بنفسك بدلًا من قبول تقريب مدموج — لكن البديل هو نموذج يُفتح على ما يرام لكنه يحسب خطأ

كيف تفشل دالة GraftPdfAcroForm في PDFiumPas بإحكام داخل Delphi: تُعاد قراءة المراجعة المكتوبة ويُتحقق من عدد حقولها، وأي شرط ملتبس مثل XFA أو صفحة غير معيّنة يرفض الاستدعاء، والرفض يتجاهل فقط مداخل الخريطة التي أضافها ذلك الاستدعاء
مسار الكتابة المُتحقَّق منه والخريطة المعاملاتية هما سبب عدم تخليف الترحيل المرفوض لملف مدموج جزئيًا أبدًا

متى يكون الترحيل الأداة الخطأ

الترحيل ينقل البنية، فاستخدمه عندما تكون البنية هي ما ينقصك. إذا كان المستندان يحملان أصلًا مجموعة الحقول نفسها ولم تكن بحاجة إلا إلى نقل القيم والتعليقات التوضيحية بينهما، فإن مسار التصدير والاستيراد في مقالة بيانات نماذج XFDF أخفّ ومعياري وقابل للعكس. الجأ إلى GraftPdfAcroForm عندما لا تحمل الوجهة أي حقول إطلاقًا، أو تحمل مجموعة مختلفة، وتحتاج إلى انتقال الودجات وتيارات المظهر والإجراءات وترتيب الحساب سليمة. ملاحظة عملية أخيرة حول الهوية: بما أن خريطة الترحيل تُرمَّز برقم الكائن مضافًا إليه رقم الجيل ومربوطة ببصمة SHA-256 لبايتات المصدر، فإن إعادة حفظ المصدر أو تحسينه بين التشغيلات تُنتج هوية مختلفة وخريطة لم تعد سارية. التقط لقطة للمصدر الذي ترحّل منه وأبقِه مستقرًا طوال الدفعة؛ تعامل معه بوصفه قطعة إدخال ثابتة، لا شيئًا يحق لمهمة ليلية إعادة كتابته

تأتي GraftPdfAcroForm وTPdfCrossDocumentGraftMap ومجموعة أدوات PDF على مستوى التيارات المحيطة بها ضمن مكوّن PDFiumPas Delphi PDFium لـ Delphi وC++Builder وLazarus، حيث تحمل صفحة المنتج المرجع الكامل لواجهة API لخيارات الترحيل وحقول التقرير وبقية سطح تحرير المستندات