כן — PDFium תומכת בטפסי XFA, עם פיצול אחד חשוב. רכיב ה-PDFium יכול לזהות טופס XFA ולחלץ את חבילות ה-template, datasets ו-config שלו כ-XML גולמי באמצעות ה-DLL הסטנדרטי, עם אפס תלות בזמן ריצה. הרצת טופס XFA *דינמי* — פריסת תיאור ה-XML שלו לעמודים חיים ואינטראקטיביים — היא בעיה נפרדת שדורשת בנוסף את הבנייה המאופשרת ב-V8. הקהילה ממשיכה לשאול האם PDFium \"עושה XFA\" כאילו מדובר ביכולת של כן-או-לא אחת, והתשובה הכנה היא שקריאת נתוני XFA ורינדור טופס XFA הם שני דברים שונים מאוד עם טביעות רגל שונות מאוד של תלויות
מאמר זה עובר על שני החצאים באמצעות PDFium Component, עטיפת ה-VCL המקורית סביב PDFium עבור דלפי ו-C++Builder. תחילה נתיב הזיהוי והחילוץ הסטטי — FormType, מאפיין XFA וקוראי ה-GetXfaPacket* — לאחר מכן נתיב זמן הריצה הדינמי עם מגבלת ה-V8 שלו, ולבסוף בדיקות היכולת המאפשרות לך להיכשל בחן כאשר DLL שנפרס אינו יכול להריץ את המנוע
האם PDFium תומכת בטפסי XFA?
התייחס לכך כשתי יכולות, ולא כאחת. חילוץ XFA סטטי זמין תמיד: ייצויי ה-FPDF_GetXFAPacket* ברמה הנמוכה שמגבים את GetXfaPacketCount, GetXfaPacketName ו-GetXfaPacketContent מקומפלים לתוך כל קובץ pdfium.dll מודרני סביר, אפילו בבנייה שקומפלה ללא XFA. זו הסיבה ששליפת ה-XML הגולמי של template, datasets ו-config מתוך טופס אינה נושאת שום תלות בזמן ריצה כלל — זוהי קריאה מבנית טהורה, ללא מעורבות של מנוע. XFA דינמי הוא היכולת השנייה: הרצה בפועל של הטופס, הערכת החישובים שלו ואפשור למנוע הפריסה של XFA למספר אותו לעמודים. נתיב זה קיים רק בבניית PDFium שקומפלה עם תמיכת XFA מעל מנוע ה-JavaScript V8, וזה החלק שרוב שרשורי ה-\"האם PDFium תומכת ב-XFA\" באמת מתווכחים עליו
התקנים ממסגרים את הפיצול בצורה נקייה. XFA מוגדר מחוץ לדקדוק הליבה של PDF ב-Adobe XFA 3.3 (2012), ו-PDF 1.7 §8.6.7 מתאר כיצד טופס XFA נישא בתוך PDF: חבילות הטופס שוכנות תחת רשומת /XFA של מילון ה-AcroForm, ומסמך שחייב להיפרס על ידי מעבד XFA לפני שיש לו משמעות כלשהי מגדיר /NeedsRendering true בקטלוג. שני הסימנים הללו הם בדיוק מה שרכיב ה-PDFium מחפש, ובדיוק מה שאתה יכול להסתמך עליו כאשר אתה מנתח קובץ שמעולם לא ראית
במה שונה XFA מטופס AcroForm?
AcroForm הוא טופס ה-PDF הקלאסי: הערות ווידג'ט (widget annotations) — שדות טקסט, תיבות סימון, קבוצות כפתורי רדיו — המעוגנים בקואורדינטות קבועות בעמודי PDF רגילים. העמוד שאתה רואה הוא העמוד שבקובץ, והשדות יושבים מעליו. XFA נוקט בגישה ההפוכה. הטופס האינטראקטיבי מתואר כולו ב-XML, ובמסמך XFA מלא (דינמי) עמודי ה-PDF הם למעשה ממלאי מקום — התוכן האמיתי מופק בזמן הפתיחה על ידי מנוע XFA הקורא את תבנית ה-XML ופורס אותה, כשהיא גדלה או מתכווצת כדי להתאים לנתונים. זה מה ש-/NeedsRendering true מכריז: ללא מעבד XFA, העמודים הסטטיים עשויים להיות ריקים או לשאת רק הודעת \"אנא פתח בצופה תואם\"
מטען הנתונים (payload) של XFA עצמו הוא קבוצה של חבילות בעלות שם, ושלוש מהן נושאות את כל מה שרוב השילובים צריכים. חבילת ה-template היא הגדרת הטופס — שדות, פריסה, חישובים וסקריפטים. חבילת ה-datasets מחזיקה את נתוני המופע בפועל, ערכי השדות הממולאים כעץ XML. חבילת ה-config נושאת הוראות עיבוד עבור מנוע ה-XFA. קיימות אחרות — localeSet, connectionSet, מעטפת xdp — אך לצורך ביקורת טופס או העברת הנתונים מתוכו, תבנית פלוס נתונים היא בדרך כלל העבודה כולה. מכיוון שנתונים אלו הם פשוט XML השוכן בזרם (stream), קריאתו מעולם לא דורשת הרצה של דבר, וזו הסיבה הבסיסית לכך שחילוץ סטטי חופשי מתלויות
זיהוי טופס XFA בדלפי
הזיהוי מתחיל בדרך שבה מתחילה כל משימה של PDFium Component: הגדרת FileName, העברת Active := True, ובדיקה שהוא אכן נפתח. Active := True לעולם אינו מעלה חריגה — קובץ חסר, סיסמה שגויה או DLL חסר פשוט משאירים את Active כ-False — ולכן ההגנה היא חובה. ברגע שהמסמך פתוח, FormType מספר לך באיזה מודל טופס הוא משתמש. שניים מערכי ה-TPdfFormType הם XFA: ftXfaFull הוא טופס XFA מלא ודינמי, מהסוג שזקוק לרינדור, בעוד ftXfaForeground הוא XFAF, תת-קבוצת החזית שבה תוכן XFA מצויר מעל עמודי AcroForm רגילים אחרת, כך שהעמודים הסטטיים נשארים בעלי משמעות. המשתנה הבוליאני XFA הוא קיצור דרך ל-\"זהו מסמך XFA מכל סוג\". אם אתה עובד גם עם השדות ברמת הווידג'ט במסמך XFAF או AcroForm, המכניקה מכוסה במדריך לניווט בשדות טפסים של PDFium
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'application-form.pdf';
Pdf.Active := True;
if not Pdf.Active then
Exit; // damaged, encrypted, or DLL missing
case Pdf.FormType of
ftXfaFull:
Log('Full (dynamic) XFA form');
ftXfaForeground:
Log('XFAF: XFA layered over static AcroForm pages');
ftAcroForm:
Log('Classic AcroForm');
else
Log('No interactive form');
end;
if Pdf.XFA then
Log('Document carries an XFA packet payload');
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
חילוץ חבילות XFA כ-XML גולמי
החילוץ הוא החצי בעל הערך הרב והחופשי מתלויות. הגן באמצעות XfaStaticReadable — הוא מאשר שייצויי קורא החבילות נפתרו ב-DLL הנטען — ואז עבור על החבילות לפי אינדקס. GetXfaPacketCount מחזיר כמה חבילות הטופס נושא, GetXfaPacketName מעניק לכל אחת את שמה, ו-GetXfaPacketContent מחזיר את הבתים הגולמיים, שעבור החבילות המוכרות הם XML בקידוד UTF-8. כאשר אתה כבר יודע איזו חבילה אתה רוצה, עוזרי השמות GetXfaTemplate, GetXfaDatasets ו-GetXfaConfig פותרים את השם עבורך ומחזירים TBytes ישירות. עבור משימת העברת נתונים, החשוב הוא GetXfaDatasets: הוא מעניק לך את ערכי השדות שנשלחו כמסמך XML שתוכל לנתח ולמפות לסכמה משלך, אותו סוג של עבודה עוקבת שתבצע לאחר חילוץ טקסט מ-PDF
procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
I, Count: Integer;
PacketName: string;
Content, Datasets: TBytes;
begin
if not Pdf.XfaStaticReadable then
Exit; // packet reader exports not present
Count := Pdf.GetXfaPacketCount;
for I := 0 to Count - 1 do
begin
PacketName := Pdf.GetXfaPacketName(I);
Content := Pdf.GetXfaPacketContent(I); // raw UTF-8 XML
TFile.WriteAllBytes(
TPath.Combine(Dir, PacketName + '.xml'), Content);
end;
// Or jump straight to the filled-in field values by name:
Datasets := Pdf.GetXfaDatasets;
if Length(Datasets) > 0 then
ParseAndMap(Datasets);
end;
מדוע רינדור XFA דינמי זקוק למנוע V8?
מסקנה תחילה: מכיוון שטופס XFA דינמי הוא תוכנית קטנה, ולא ציור סטטי. חבילת התבנית (template) נושאת חישובי FormCalc ו-JavaScript, אימותים וסקריפטים של אירועים, וממנוע הפריסה של XFA חייב להריץ אותם כדי להחליט כיצד העמודים בכלל נראים. PDFium מיישמת מנוע זה רק בבנייה שקומפלה עם תמיכת XFA מעל V8, מנוע ה-JavaScript של Google — אותה סיבה לכך שה-DLL המאופשר ב-V8 (pdfium32v8.dll או pdfium64v8.dll) גדול באופן ניכר מהבנייה הסטנדרטית. ללא V8 אין מי שיריץ את הסקריפטים, ולכן אין עמודים מרונדרים להציג, אלא רק את ה-XML הגולמי שאתה כבר יכול לקרוא סטטית
לבחירת בנייה זו יש מגבלה קשיחה שאינך יכול לעקוף בעיצוב: זוהי החלטה חד-פעמית לכל תהליך. תהליך יחיד אינו יכול לטעון הן את pdfium.dll הסטנדרטי והן את הבנייה המאופשרת ב-V8 — הם מייצאים את אותם סמלים ו-V8 שומר על מצב isolate גלובלי — סו PDFium Component חייב לבחור בבניית ה-V8 לפני שהספרייה נטענת בפעם הראשונה בהחלט. כדי להפוך זאת לאוטומטי, LoadDocument סורק מראש את הקובץ לאיתור סימני /XFA ו-/NeedsRendering true (אותה בדיקה החשופה כפונקציה העצמאית PdfFileLooksXfa) ומגדיר EnableV8Engine := True לפני הטעינה כשהוא מוצא אותם. ברגע ש-pdfium.dll רגיל כבר שוכן בזיכרון, השינוי אינו יכול עוד להיכנס לתוקף, ומקרה זה הוא מה שנתיב חוסר זמן הריצה מדווח עליו
begin
// Peek for XFA before PDFium is loaded for the first time, so the
// V8-enabled build can still be selected. PdfFileLooksXfa scans for
// /XFA and /NeedsRendering true without parsing the whole file.
if PdfFileLooksXfa('dynamic-form.pdf') then
EnableV8Engine := True;
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'dynamic-form.pdf';
Pdf.Active := True;
if Pdf.Active and Pdf.XFA then
begin
if Pdf.XfaRuntimeAvailable then
RenderXfaPages(Pdf) // engine is live: dynamic layout
else
ExtractXfaData(Pdf, 'C:\out'); // fall back to static packets
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
טיפול בחן בחוסר של זמן ריצה של XFA
שלוש בדיקות מספרות לך בדיוק כמה יכולת XFA יש לך, וקוד כנה בודק אותן במקום להניח. XfaFeaturesAvailable היא בדיקה גלובלית שעוזרי הייצוא של XFA/V8 קיימים ב-DLL הנטען. XfaStaticReadable מאשר, לכל מסמך, שקוראי החבילות נפתרו — ההגנה לחילוץ. XfaRuntimeAvailable הוא החזק: הוא true רק כאשר מסמך ספציפי זה הפעיל בפועל את מנוע ה-XFA, כלומר הוא XFA, בניית ה-V8 נטענה ו-FPDF_LoadXFA הצליח. מסמך יכול לדווח על XFA = True אך XfaRuntimeAvailable = False בכל פעם של-DLL שנפרס חסר XFA או שהבנייה הסטנדרטית כבר נטענה
כאשר מסמך XFA נפתח אך המנוע אינו יכול לרוץ, PDFium Component אינו מהמר. הוא מקבע את הטופס למצב הסטטי הבטוח במקום לבקש זמן ריצה דינמי שאינו יכול לכבד, והוא מעלה את OnXfaRuntimeMissing פעם אחת כדי שהמארח יוכל להגיב — בדרך כלל על ידי הנחיית המשתמש להפעיל מחדש עם הבנייה המאופשרת ב-V8, תוך המשך הצעת הנתונים הקריאים סטטית בינתיים. חיבור מטפל זה הוא ההבדל בין נסיגה (fallback) נקייה לבין טופס שאינו מציג דבר בשקט
procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Fires once when an XFA document opens but the engine cannot run:
// a plain pdfium.dll was already loaded, or the build lacks XFA/V8.
ShowMessage('This is a dynamic XFA form. Restart with the ' +
'V8-enabled build to render it; its packet data is still readable.');
end;
היה כנה לגבי הגבול גם במוצר שלך. אם אינך שולח לעולם את ה-DLL המאופשר ב-V8, טפסי XFA דינמיים לעולם לא ירונדרו עבור המשתמשים שלך — אך עדיין תוכל לזהות אותם, לחלץ כל חבילה ולשלוף את הנתונים שנשלחו, מה שמספיק לקבוצה הגדולה של משימות שעוסקות באמת בהוצאת נתונים מטפסים ממשלתיים ובנקאיים מיושנים במקום להציג אותם בצורה אינטראקטיבית. שמור את בניית ה-V8 הכבדה יותר למקרים שבאמת זקוקים לטופס חי וניתן למילוי, ותן לבדיקות היכולת לנתב כל מסמך לנתיב שהוא אכן יכול לקחת. אם צינור החילוץ שלך מטפל גם בקבצים מוטמעים, אותה פילוסופיית קריאה בלבד חלה על קבצים מצורפים ב-PDF בדלפי
ממשקי ה-API של FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig ובדיקות היכולת המוצגים כאן הם חלק מ-PDFium Component עבור דלפי ו-C++Builder