مقال تقني

تحويل نماذج XFA الساكنة إلى حقول AcroForm داخل Delphi

يحول PDFium Component نماذج XFA الساكنة إلى حقول AcroForm عادية من Delphi عبر FlattenPdfXfaToAcroForm في وحدة FPdfDocumentXfaFlatten. تقرأ الدالة حزمتي قالب XFA و datasets، وتقابل كل حقل موضع مطلقاً حقلاً طرفياً من AcroForm بتدفق مظهر خاص به، وتلحق النتيجة مراجعةً تدريجية، فيفتح ملف PDF المحول نموذجاً عادياً في عارضات لم تدعم XFA قط

الدافع عادة تذكرة دعم لا بند خطة. نموذج حكومي أو تأميني أُلف في مصمم حفظه XFA، يعمل في قارئ مكتبي واحد، وفي كل مكان آخر يفتح صفحة فارغة تقول إن المستند يتطلب عارضاً أحدث. أُهملت XFA في PDF 2.0‏ (ISO 32000-2)، ولم تنفذها المتصفحات والعارضات المحمولة قط، ولا تعرض PDFium نفسها XFA إلا بالبناء الممكن بـ V8. نماذج XFA الساكنة، التي تخطيطها مثبت عند وقت التصميم، تحمل في القالب هندسة كافية لإعادة بنائها حقول AcroForm، وهذا بالضبط الشريحة التي يعالجها هذا المحول

ماذا ينتج FlattenPdfXfaToAcroForm فعلاً؟

ينتج FlattenPdfXfaToAcroForm مراجعة AcroForm على مستوى البايتات ملحقة بنسخة غير متغيرة من المصدر، ولا يعيد أبداً كتابة الملف الأصلي. لا تكشف PDFium واجهة عامة لإنشاء حقول نماذج، فلا يمر المكوّن بـ PDFium لهذه المهمة إطلاقاً: يقرأ المراجعة النشطة بقارئ كائناته الخاص، ويبني قواميس حقول وتعليقات widget و XObjects مظهر ومورداً خط /Helv كائنات PDF خاماً، ويسلسلها تحديثاً تدريجياً متفرقاً بمعنى ISO 32000-1 §7.5.6

يحمل قاموس /AcroForm الجديد ‏/Fields و /DA و /DR دون مدخل /XFA. عندما يبقي المصدر AcroForm الخاصة به كائناً غير مباشر يُعاد كتابة ذلك الكائن في مكانه؛ ويُستبدل القاموس المضمّن داخل الكتالوج. وفي الحالين لا تشير المراجعة النشطة بعد الآن إلى حزم XFA، فيفتح العارض الذي يفهم النموذجين حقول AcroForm بدل محاولة تخطيط نموذج XFA. بايتات XFA الأصلية ما تزال في المراجعة الأسبق، وهذا ما يعنيه تحديث تدريجي، لكن لا شيء في المراجعة الحالية يشير إليها

أنبوب تحويل XFA الساكنة إلى AcroForm في PDFium Component لـ Delphi: تقرأ FlattenPdfXfaToAcroForm حزمتي القالب و datasets، وتقابل الحقول موضعاً مطلقاً قواميس حقول و widgets، وتلحق AcroForm مراجعةً تدريجية متفرقة دون مدخل /XFA
لا تمس FlattenPdfXfaToAcroForm البايتات الأصلية قط: تلحق الحقول المحولة و widgets وتدفقات المظهر مراجعة يتوقف فيها /AcroForm ببساطة عن الإشارة إلى حزم XFA
uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;

procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
  Source: TFileStream;
  Options: TPdfXfaFlattenOptions;
  OutputPdf: TBytes;
  Report: TPdfXfaFlattenReport;
  Fields: TPdfXfaFlattenFields;
begin
  Options := TPdfXfaFlattenOptions.Default;
  Options.MaxFields := 1024;   // الافتراضي 512 والمدى المسموح 1..4096

  Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
  try
    if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
      raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
  finally
    Source.Free;
  end;

  TFile.WriteAllBytes(TargetFile, OutputPdf);
  Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
    [Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;

تعيد الدالة False وتملأ Report.ErrorText لأي مدخل مرفوض أو مشوه، وتعيد موضع تدفق المصدر في طريقها للخارج. الإلغاء هو مسار الاستثناء الوحيد الذي ينتشر: إذا انطلق Options.CancellationToken أُعيد رفع EPdfOperationCancelled بعد ملء التقرير. ويحمل TPdfXfaFlattenReport أيضاً SourceSha256 و OutputSha256، وهو ملائم عندما يلزم لوظيفة دفعية أن تثبت أي مدخل أنتج أي ملف محول. يضبط TPdfXfaFlattenOptions.Default ميزانية حزم 8 MiB، و512 حقلاً، وسقف مخرجات 512 MiB؛ ولا يجوز أن يكون المصدر مشفراً

قراءة حزمتي القالب و datasets

يقبل PDFium Component التخطيطين اللذين يسمح بهما ISO 32000-1 §12.7.8 لمدخل /XFA: صيغة المصفوفة التي تناوب أسماء الحزم ومراجع التدفقات، وصيغة XDP ذات التدفق الواحد حيث يحمل تدفق واحد مستند <xdp:xdp> كاملاً. قد تكون تدفقات الحزم غير مضغوطة أو /FlateDecode دون منبئ، ويُحد فك الضغط بـ MaxPacketBytes

حالة التدفق الواحد هي حيث ينهار التعامل الساذج مع XML. يكتب المصممون عادة <xfa:datasets> ببادئة نطاق بينما يجلس القالب بلا بادئة، فالتقطيع باسم الوسم الحرفي يجد حزمة ويفوّت الأخرى. يطابق المحول أسماء العناصر والسمات بالاسم المحلي، بعد فحص منفصل بأن أزواج وسوم الفتح والإغلاق تتطابق باسمها الكامل المسبوق. وإن كنت تحتاج فقط إلى النظر في الحزم لا تحويلها فتغطي دليل كشف نماذج XFA واستخلاص حزمها بواسطة PDFium VCL ذلك المسار القرائي فقط

قيم الحقول تأتي من موضعين. عنصر <value> في القالب يزود الافتراضي، وحزمة datasets تزود البيانات المعبأة، مربوطة باسم الحقل. تتكرر أسماء الحقول في النماذج الحقيقية، فيقارن الرابط سلسلة أجداد <subform> المسماة بمسار الأجداد في البيانات ويأخذ أطول مطابقة غير متضاربة. وعندما يتشارك حقلا ورقة اسماً تحت subforms مختلفة يصير الـ /T في AcroForm ‏SubformPath.Name فيبقى الحقلان مميزين بعد التحويل

لماذا يحتاج choiceList علماً Combo صريحاً؟

لدى XFA و PDF افتراضان متعاكسان لحقول الاختيار، فالمحول الذي لا ينسخ أعلاماً يحول كل قائمة منسدلة إلى صندوق قائمة. ‏<choiceList> في XFA ذات open="userControl" الافتراضية أو ذات open="onEntry" قائمة منسدلة؛ وحقل /FT /Ch في PDF بلا أعلام صندوق قائمة ممرِّغ، وعلم Combo وحده (موضع البت 18 في الجدول 230 من ISO 32000-1، القيمة 131072، البت نفسه الذي تسميه PDFium ‏FPDF_FORMFLAG_CHOICE_COMBO) يجعله صندوق تحرير وسرد. لذلك يضبط المحول علم Combo لحالات المنسدلة ويرفع الأعلام فارغة فقط عندما يقول القالب open="always" أو open="multiSelect"، ما يبقي القائمة مفتوحة دوماً. قلبت الإصدارات قبل 3.181.1 قيمتي Combo و Pushbutton وبحثت عن open="on" غير قياسية، فالنماذج المسطحة بها تستحق إعادة التشغيل

قرار علم حقل الاختيار في تحويل XFA الساكنة إلى AcroForm في PDFium Component: تضبط سمة open بقيمة userControl أو onEntry علماً Combo، البت 18 بقيمة 131072، لإنتاج صندوق تحرير وسرد منسدل، بينما ترفع open بالقيمتين always و multiSelect الأعلام فارغة لصندوق قائمة مفتوح دائماً
دون علم Combo تنبسط قائمة XFA المنسدلة صندوق قائمة ممرغاً، فيقرأ المحول سمة open ويضبط البت 18 لحالات المنسدلة فقط

بقية مطابقة الواجهة تتبع جداول أعلام الحقول في ISO 32000-1 §12.7.4:

  • textEdit و numericEdit و dateTimeEdit تصير /FT /Tx؛ ويحصل حقل نص متعدد الأسطر على علم Multiline‏ (/Ff 4096، الجدول 228) وتدفق مظهر مقسم الأسطر
  • تصير passwordEdit ‏/FT /Tx بعلم Password‏ (/Ff 8192) ومظهر يرسم نجمة واحدة لكل scalar في Unicode لا لكل وحدة كود UTF-16
  • تصير checkButton صندوق اختيار /FT /Btn تكون قيمته /Yes عندما تكون البيانات المرتبطة 1 و /Off خلاف ذلك
  • تصير <exclGroup> أباً radio واحداً /FT /Btn /Ff 32768‏ (الجدول 226) بـ /V و /DV و /Opt و /Kids، مع widget ابن لكل عضو بـ /AS خاصة و /AP ثنائية الحالة
  • تصير imageEdit زر دفع يرسم مظهره الصورة؛ وتغرز JPEG أساسية حرفياً تحت /DCTDecode، بينما تفك PNG ترميزها وتعيد ترميزها بكسلات /FlateDecode مع /SMask إضافية عندما تحمل PNG ألفا أو شفافية tRNS

القيم التي ليست ASCII قابلة للطباعة خالصة، بما فيها فواصل الأسطر، تُكتب سلاسل <FEFF...> بصيغة UTF-16BE، فينجو اسم ياباني أو عنوان من سطرين في الرحلة. ويطابق مخرج Fields ما كُتب، وهو ما يجعله مسار تدقيق ملائماً

procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
  KindNames: array[TPdfXfaFlattenFieldKind] of string = (
    'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
  I: Integer;
  F: TPdfXfaFlattenField;
begin
  for I := 0 to High(Fields) do
  begin
    F := Fields[I];
    Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
      [F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
    case F.Kind of
      fxfkChoice:
        Writeln(Format('  %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
      fxfkRadio:
        Writeln(Format('  %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
      fxfkImage:
        Writeln(Format('  %d image bytes', [Length(F.ImageData)]));
    else
      Writeln('  value "' + F.Value + '"');
    end;
  end;
end;

الهندسة ووحدات القياس وتوجيه الصفحات

تتحول هندسة XFA إلى فضاء مستخدم PDF بقلب كل حقل مقابل ارتفاع MediaBox لصفحته الخاصة، لأن XFA تقيس نزولاً من أعلى الصفحة و PDF تقيس صعوداً من الأسفل. تتراكم الإزاحات من عناصر <subform> المتداخلة في النزول عبر القالب، ويجب أن ينتهي كل حقل بعرض وارتفاع موجبين وإلا رُفض التحويل

تحويل هندسة XFA إلى PDF في PDFium Component لـ Delphi: يُقلب مستطيل حقل مقيساً نزولاً من الحافة العليا في قالب XFA مقابل ارتفاع MediaBox لصفحته الخاصة إلى فضاء مستخدم PDF، حيث ينمو الإحداثي y صعوداً من الأسفل
يلتقي قالب XFA النازل وفضاء مستخدم PDF الصاعد في عملية طرح واحدة مقابل ارتفاع MediaBox، وهي ما يضع كل widget محول في موقعه المصمم

تقبل القياسات الوحدات التي تبثها المصممات الحقيقية: أعداداً مجردة و pt، مع in‏ (72 نقطة)، و mm‏ (72 / 25.4)، و cm‏ (72 / 2.54)، و pc‏ (12)، و px‏ (0.75)، و mp‏ (0.001، ميلي نقاط). التحليل مستقل عن المحلية، فتثبيت Windows ألماني لا يحول 8.5in إلى هراء. ولاحقة وحدة مجهولة تُرفض بدل قرائها نقاطاً، وهو ما كان سينقل الحقل خطأً بصمت

يتبع توجيه الصفحات <pageSet>: تُرقَّم عناصر <pageArea> الشقيقة من واحد بترتيب التصريح وتطالب بالحقول المتداخلة تحتها، بينما تبقى الحقول بلا جدّ <pageArea> في الصفحة الأولى. وينال كل صفحة تحديث /Annots خاصاً بها. حقل يُوجَّه بعد الصفحة الأخيرة في الـ PDF خطأ لا widget أُلقى في الصفحة الأولى

أي نماذج XFA يرفضها المحول؟

يرفض المحول أي شيء لا يمكن معرفة تخطيطه دون تشغيل محرك تخطيط XFA. ذلك يعني سمة layout غير position على <subform> أو <exclGroup>‏ (المتدفق tb و lr-tb وأشباههما، وهو ما تُبنى منه النماذج الديناميكية)، وأي عنصر <script>، وأي نوع واجهة خارج السبعة المسرودة أعلاه. النماذج الديناميكية التي تكبر صفحاتها وقت التشغيل تخص سلسلة أدوات أخرى؛ وتوضح مقالة إعادة تقسيم صفحات XFA الديناميكية وعدّ الصفحات في PDFium Component ما يتضمنه ذلك التخطيط الحي

الرفض مقصود. subform المتدفق بلا إحداثيات ثابتة، فأي «تحويل» سيضطر لاختراع مواضع، والنموذج المبرمج غالباً يحسب مجاميعاً أو يبدل ظهوراً لا يستطيع AcroForm ساكن استنساخها. إنتاج PDF يبدو تاماً لكنه فقد بصمت مجموعاً محسوباً أسوأ من خطأ واضح في Report.ErrorText. تعرف مواصفة XFA 3.3 أكثر بكثير من هذه المجموعة الفرعية، والمحول لا يتظاهر بخلاف ذلك

مزالق عند كتابة مراجعة AcroForm والتحقق منها

برزت مشكلتان أثناء بناء الكاتب، وكلتاهما تنطبق على كل من ينتج تحديثات تدريجية يدوياً. الأولى: يجب أن يكون الكتالوج دوماً جزءاً من مجموعة التحديث، حتى عندما لا تتغير بايتاته لأن AcroForm كائن غير مباشر يُعاد كتابته في مكان آخر. يشتق كاتب التحديث المتفرق المقطع الختامي /Root من الكائنات التي يحملها، والتحديث دون كائن الجذر ينتج مراجعة لا تستطيع العارضات مرساتها

الثانية تخص التحقق. تحميل الملف المحول عبر غلاف PDFium يطلق تهيئة بيئة النموذج بمجرد أن يحوي المستند /AcroForm، وكان ملف pdfium.dll المضيف في إعداد الاختبار ينقصه التصدير FORM_GetTextDirection، ففشلت التهيئة قبل فحص أي حقل. لذلك تستخدم فحوصات القراءة العائدة قارئ كائنات المكوّن، الذي يحلل المراجعة النشطة دون لمس طبقة النماذج

uses
  System.SysUtils, System.Classes, FPdfCompress;

function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
  Stream: TBytesStream;
  Objects: TPdfActiveObjects;
  Revision: TPdfSparseRevisionInfo;
  ErrorText: string;
begin
  Stream := TBytesStream.Create(Pdf);
  try
    if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
      Objects, Revision, ErrorText) then
      raise Exception.Create(ErrorText);
    // يجب أن يشير الكتالوج النشط إلى AcroForm؛ و AcroForm مضمّن
    // كتبه المحول لا يحمل مدخل /XFA
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

تدفقات المظهر بسيطة عن قصد: Helvetica بمقاس 10 نقاط لقيم النص والاختيار، ومربع مرسوم حدوداً لصناديق الاختيار، ومربع داخلي معبأ لحالة الـ radio المنتقاة. تجعل النموذج المحول مقروءاً في كل مكان، لكنها ليست نسخة بكسلية من عرض XFA الأصلي، فالخطوط والألوان والحدود المعرفة في القالب لا تُنقل. وإن كان لا بد أن يبدو نموذج مطابقاً لمخرج المصمم فاعرضه مرة بالبناء الممكن بـ XFA وقارن قبل تحويل أنبوب إنتاج

FAQ

هل يحتاج FlattenPdfXfaToAcroForm بناء V8 أو XFA من PDFium؟

لا. يحلل FlattenPdfXfaToAcroForm الحزم ويكتب المراجعة بكود PDFium Component، فيعمل مع pdfium.dll القياسي ولا ينفذ أي سكربت XFA

هل يعالج المحول مستندات XFA المشفرة؟

ليس مباشرة. يُرفض المصدر المشفر بخطأ، ففك تشفير المستند أولاً وحوّل النسخة المفكوكة

هل تظل الحقول المحولة قابلة للتحرير؟

نعم. يحوي المخرج حقول AcroForm حقيقية للنص وصناديق الاختيار و radio والاختيار وأزرار الدفع بقيمها وتدفقات مظهرها، فيستطيع المستخدمون مواصلة ملء النموذج في أي عارض يدعم AcroForm

تحويل XFA الساكنة واستخلاص حزم XFA وزمن تشغيل XFA الممكن بـ V8 كلها من PDFium Component لـ Delphi و C++Builder، الذي يغلّف PDFium لمشاريع Delphi و C++Builder و Lazarus