PDFium Component שומר ערכי טפסי XFA ערוכים בדיוק מלא, על פני שמירה ופתיחה מחדש, כשהוא מריץ את הרנטיים של ה-V8 ל-Windows pdfium.v8.dll שנשלחו ב-v3.125.2 ומעלה. runtimes ישנים יותר הוסיפו ירידות שורה לערכי שדות, קטעו אימוג'י אל תו BMP לא קשור, דילגו בשקט על שמירות XFA של stream בודד ויכלו לבלוע כתיבה סופית שנכשלה. סימפטום פתיחה מחדש אחד אינו פגם של הספרייה בכלל: טופס דינמי שה-subform השורשי שלו חסר restoreState="auto" בונה את הפריסה שלו מחדש מהתבנית
דיווחי הבאגים על כל אלה נראו דומים. לקוח ממלא טופס תביעה של XFA בצופן Delphi, שומר, פותח מחדש, ומשהו סוטה מעט. תיבת הערות ריקה מחזיקה עכשיו שורה ריקה, ואחרי שמירה שנייה היא מחזיקה שתיים. שם שהוקלד עם אימוג'י חוזר עם גליף מאזור שימוש פרטי. אף אחד לא מקבל שגיאה, וזה בדיוק מה שהופך את הבאגים האלה ליקרים: הסטייה עולה שבועות אחר כך בייצוא של מישהו אחר
מה משתבש כשטופס XFA נשמר ונפתח מחדש?
ארבעה פגמים נפרדים בנתיב השמירה הילידי של XFA גרמו לסטיית ערכים, וכל אחד הסתתר מאחורי שמירה שנראתה מוצלחת. שניים הגיעו מסריאליזציה, אחד מפריסת האחסון של stream בודד, ואחד מכותב ה-PDF עצמו. הטבלה ממפה כל סימפטום אל הסיבה שלו ואל המהדורה שבה PDFium Component תיקנה אותו
| סימפטום אחרי פתיחה מחדש | סיבה | תוקן ב |
|---|---|---|
| שדה ריק מחזיק ירידת שורה; ערכים מוסיפים newline בכל שמירה | שני כותבי ה-XFA הכניסו newlines של פריסה אחרי תגיות פתיחה | v3.125.2, pdfium.v8.dll |
| U+1F642 חוזר בתור U+F642, או שהאימוג'י נעלם מחבילת הטופס | קטיעת wchar_t בת 16 ביט בפענוח; סינון surrogates במסדר הטפסים | v3.125.2, pdfium.v8.dll |
| עריכות במסמך XFA של stream בודד פשוט נעלמות | השמירה הילידית דחתה את פריסת ה-stream, אבל את ערך ההחזרה התעלמו | v3.125.2; הערות והוראות עיבוד נשמרות מאז v3.126.0 |
| קובץ קטום למרות שהשמירה דיווחה הצלחה | כתיבת החוצץ הסופית נכשלה אחרי שהכותב כבר דיווח הצלחה | רנטיים V8 ב-v3.125.2; ה-pdfium.dll הרגילה ב-v3.125.3 |
| טופס דינמי של שלושה עמודים נפתח מחדש בתור שני עמודים | ה-subform השורשי אינו מבקש restoreState="auto" | כתיבת טפסים, לא פגם של ספרייה |
רשימות כתובות קודמות הסיקו שעריכות שדות XFA לא יכולות בכלל להישמר עם PDFium, מה שהיה מדויק עבור ה-runtimes של אז. ה-rntime החדש של ה-V8 שומר ערכי XFA באופן ילידי, ולכן עריכה שנעשתה בטופס החי מגיעה אל חבילת ה-datasets השמורה בלי ניתוח חבילות בעצמכם
איזה רנטיים PDFium שומר ערכי XFA?
נאמנות שמירת ה-XFA תלויה ב-DLL הילידית, לא בעוטף של Delphi, ולכן הבדיקה הראשונה היא איזה runtime התהליך שלכם באמת טען. PDFium Component שולחת שני builds ל-Windows לכל ארכיטקטורה: את ה-pdfium.dll הרגילה, שנבנתה בלי V8 ובלי XFA, ואת pdfium.v8.dll, שנושאת את מנוע ה-JavaScript ואת ה-rntime של טפסי ה-XFA. רק pdfium.v8.dll יכולה להריץ טופס XFA, ולכן כל תיקון XFA שתואר כאן חי בה, החל מספריות ה-V8 הבנויות מחדש ל-Win32 ול-Win64 ב-v3.125.2
תיקון הכתיבה הסופית הוא קוד כותב PDF כללי, ולכן הוא חשוב גם למסמכים רגילים. v3.125.3 בנתה מחדש את ספריות ה-pdfium.dll הרגילות כדי שינשאו את אותו תיקון. מקור משותף אינו הוכחה להתנהגות משותפת: עד שהבינארי נבנה מחדש, ה-DLL הישנה שומרת על הבאג הישן
מלכודת שנייה ישבה ב-loader. לפני v3.125.2, הגדרת EnableV8Engine ל-True גרמה ל-binding לבחור את שם ברירת המחדל pdfium.v8.dll ולהתעלם מנתיב מלא ב-LibraryName. אפליקציה שכיוונה אל runtime שנפרס זה עתה יכלה להמשיך לטעון עותק ישן יותר מתיקייה אחרת. מאז v3.125.2, LibraryName שמכיל תיקייה בוחר בדיוק את הקובץ הזה בשני מצבי המנוע, ונתיב חסר נכשל במקום ליפול בחזרה אל ספרייה מצורפת אחרת
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// תיקייה ב-LibraryName נועלת את הקובץ המדויק הזה (v3.125.2 ומעלה);
// אם הקובץ חסר, הטעינה מעלה חריגה במקום ליפול בחזרה
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // נכשלים בהפעלה, לא בשמירה הראשונה
end;
אחרי פתיחת מסמך, TPdf.XFA אומר לכם שהקובץ מכיל XFA ו-TPdf.XfaRuntimeAvailable אומר שה-DLL הטעונה באמת יכולה להריץ אותו. אם צריך גם להבחין בין טפסים סטטיים ודינמיים, TPdf.FormType מחזיר ftXfaFull או ftXfaForeground; המאמר על זיהוי טפסי XFA וחילוץ חבילות XFA ב-Delphi מכסה את הבדיקות האלה בפירוט
מדוע שדות XFA שנשמרו מרוויחים ירידות שורה נוספות?
שדות XFA שנשמרו הרוויחו ירידות שורה כי שני כותבי ה-XFA הילידיים, כותב אלמנטי ה-XML הכללי ומסדר חבילות הטופס, עיצבו את הפלט שלהם ביפוי עם newline אחרי תגיות פתיחה. ברוב ה-XML הרווח הזה קוסמטי. בנתוני XFA הוא אינו כזה: כשחבילת ה-datasets מפורסרת שוב, הטקסט בין <Comments> ל-</Comments> הוא ערך השדה, ירידת השורה כלולה. שדה ריק לכן נפתח מחדש כשהוא מחזיק LF אחד, וכל מחזור שמירה-ופתיחה נוסף יכל להוסיף עוד אחד
התיקון המובן מאליו, חיתוך ערכים בטעינה, היה שגוי. משתמשים מקלידים רווחים פותחים, רווחים עוקבים וטקסט מרובה שורות בכוונה לתוך שדות XFA, ובלוק כתובת או קוד ברוחב קבוע חייבים לשרוד בייט-בייט. תיקון v3.125.2 לכן מסיר רק את הרווח שהמסדר עצמו סינתז סביב תגיות. ערכי משתמש, צמתי טקסט קיימים ומקטעי CDATA עוברים ללא מגע, ולכן " indented" נשאר מוזח ושדה שנועד להיות ריק נשאר ריק
מדוע אימוג'י חוזר בתור תו אחר?
אימוג'י חזר שגוי כי wchar_t של Windows הוא ברוחב 16 ביט, ושני נתיבי פענוח אחסנו ערך סקלר מלא של Unicode ב-wchar_t בודד. מפענח ה-stream של UTF-8 והמפרסר של הפניות תווים מספריות כמו 🙂 שניהם עשו זאת. U+1F642, הפרצוף המחייך במקצת, לא נכנס ל-16 ביט, ולכן הביטים הגבוהים נשרו ו-U+F642 הופיע במקום: code point באזור השימוש הפרטי שרוב הגופנים מרנדרים בתור ריבוע או כלום
למסדר הטפסים הייתה הבעיה ההפוכה. הוא סינן תווים wchar_t אחד בכל פעם, ראה שתי יחידות קוד surrogate שאינן תקפות בבידוד, והשליך את שתיהן, ולכן האימוג'י נעלם מחבילת הטופס לגמרי. ב-v3.125.2 המפענח צורך כל ערך סקלר במלואו ופולט זוג surrogate תקין. כשנותר מקום פלט אחד בלבד, הוא משאיר את ה-surrogate הנמוך ממתין ואינו מדווח סוף-stream בזמן שהיחידה עדיין בחוצץ. רצף UTF-8 שנחתך בין בלוקי קריאה נישא אל הקריאה הבאה במקום להיזרק. מייצא הטפסים משאיר עכשיו זוגות surrogate תקפים יחד, והפניות תווים מספריות מפיקות זוגות נכונים גם כן
נתוני בדיקה מ-Latin-1 לעולם לא מציגים אף אחד מאלה, ולכן לכל בדיקת round-trip של XFA צריך לפחות תו אחד מהמישור המשלים
XFA של stream בודד וכשלי שמירה שאף אחד לא ראה
מסמך XFA של stream בודד איבד את עריכותיו כי עוזר השמירה הילידי דחה את פריסת האחסון הזאת והקורא שלו התעלם מהכשל. ISO 32000-1 §12.7.8 מתיר לרשומת ה-/XFA של מילון הטופס האינטראקטיבי להיות או מערך של שמות חבילות ו-streams, או stream בודד שמחזיק את מסמך ה-XDP כולו. מערכי חבילות הם המקרה הנפוץ, אבל streams בודדים חוקיים לגמרי, ושמירת ה-PDF הסתיימה כאילו דבר לא קרה בזמן שנתוני הטופס נשארו בערכיהם הישנים
מאז v3.125.2, ה-rntime של ה-V8 מטפל בתת-הקבוצה הנתמכת של stream בודד. הוא קודם מייצא את שתי החבילות החיות, datasets ו-form, אל אזור הכנה ומאמת אותן, ורק אז מחליף את החבילות התואמות בתוך ה-XDP המקורי. שאר החבילות והצהרות מרחבי השמות של השורש נשמרות. אם ההכנה נכשלת, ה-stream המתמשך של ה-XFA לעולם לא נגוע והמסמך שומר על סימון השינוי שלו
הערות XML והוראות עיבוד דרשו טיפול נוסף כי ה-DOM הפנימי של ה-XML משליך אותן. ב-v3.125.2 נוכחותן גרמה לשמירה להיכשל לחלוטין במקום לאבד תוכן בשקט. v3.126.0 משמר אותן: לפני הפרסור, כל הערה או הוראת עיבוד מוחלפת במנעול שנבנה מקידומת שאינה מופיעה בשום מקום בטקסט המקורי. אחרי שהחבילות החיות מוחלפות, כל מנעול חייב להופיע בדיוק פעם אחת לפני שה-token המקורי משוחזר וה-stream נכתב. Tokens מחוץ לחבילות המוחלפות לכן שומרים על הטקסט והסדר שלהם, כולל tokens ב-prolog, ב-template ובחבילות אחרות
כמה קלטים עדיין נדחים בכוונה, וכל דחייה היא כשל שמירה מפורש:
- הערות או הוראות עיבוד בתוך החבילות החיות של
datasetsאוform, מכיוון שאת מיקומן המקורי לא ניתן למפות אל תוכן שיוצא מחדש - הצהרות DTD וחתימות XMLDSig, מכיוון ששכתוב ה-XDP לא יכול לשמר חתימת XML תקפה
- קידוד UTF-8 או UTF-16 לא תקף, תגיות לא שלמות, הפניות תווים לא תקפות, ישויות לא מוכרות והוראות עיבוד משובשות, שנדחות במקום לתוקן בשקט
הפלט של stream בודד הוא UTF-8 ומשמר את מודל התוכן של ה-XML, לא את פריסת הבייטים המקורית או את הצהרת הקידוד
הפגם האחרון ישב מתחת ל-XFA. הכותב הילידי של הקבצים חוצץ פלט בבלוקים של 32 KB ושטף את הבלוק החלקי הסופי רק ב-destructor שלו, אחרי שכותב המסמך כבר דיווח הצלחה. דיסק מלא או שגיאת I/O על אותו בלוק אחרון היו בלתי נראים לקורא. מאז v3.125.2 ב-rntime של ה-V8 ומאז v3.125.3 ב-rntime הרגיל, ה-flush הסופי הזה הוא חלק מתוצאת השמירה, וסימון השינוי של ה-XFA מנוקה רק אחרי הצלחה אמיתית. בצד Delphi, TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean כותב אל קובץ זמני לצד היעד ומזיז אותו למקום רק כשהשמירה מחזירה True, ולכן שמירה שנכשלה משאירה את הקובץ הקודם שלם
מדוע טופס dynamic XFA נפתח מחדש עם פחות עמודים?
טופס dynamic XFA נפתח מחדש עם פחות עמודים כשה-subform השורשי שלו אינו מצהיר restoreState="auto", וזו החלטת כתיבת טפסים ולא פגם של PDFium Component. ב-XFA 3.3, restoreState על ה-subform השורשי מוגדר כברירת מחדל manual. תחת manual, מעבד ה-XFA משחזר רק מצב מוגבל מחבילת הטופס השמורה ומשאיר את השאר לתסריטים של המחבר. ערכי שדות שנשמרו ומספרי מופעים של subforms חוזרים עדיין, אבל תכונות גיאומטריות שנקבעו בזמן ריצה לא
המקרה שחשף זאת היה טופס של שלושה עמודים שהתסריט שלו הגדיל subform אל h="450pt". חבילת הטופס השמורה החזיקה את הגובה החדש, את הערכים ואת מספרי המופעים. בפתיחה מחדש, לעומת זאת, הפריסה נבנתה מחדש מהגבהים של התבנית והטופס זרם מחדש אל שני עמודים. ה-rntime היה צודק: התבנית מעולם לא ביקשה שחזור אוטומטי. ההצהרה על ה-subform השורשי מתקנת את הפתיחה מחדש:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- שדות; תסריטים עשויים לשנות h או להוסיף מופעים בזמן ריצה -->
</subform>
</subform>
</template>
אם התבנית אינה בבעלותכם, אל תטלאו סביבה בצופה: טופס שנשען על מצב manual מצפה שהתסריטים שלו עצמם יבנו את המצב מחדש. רפאגינציה חיה בזמן שהמשתמש מקליד היא נושא נפרד, מכוסה ב-איך PDFium Component עוקב אחרי מספרי עמודים של dynamic XFA ושדות שהוזזו
איך מאמתים שמירת XFA ב-Delphi?
בדיקת השמירה של XFA היחידה שאפשר לסמוך עליה היא לפתוח מחדש את הקובץ השמור במופע TPdf טרי ולקרוא את הנתונים השמורים בחזרה. TPdf.GetXfaDatasets מחזיר את חבילת ה-datasets כפי שהיא שמורה במסמך, ולא את מודל הנתונים החי של ה-XFA, ולכן קריאה לפני שמירה מציגה את הערכים הישנים. אחרי פתיחה מחדש הוא מציג בדיוק את מה שנכתב. למסמך של stream בודד אין חבילות בשמות נפרדים: PDFium מדווחת על ה-XDP כולו בתור חבילה אחת בשם ריק, ולכן GetXfaPacketByName('datasets') ו-GetXfaDatasets לא מחזירים דבר, וה-fallback קורא את ה-stream המלא דרך GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // פריסת מערך חבילות
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // stream בודד: חבילה אחת ללא שם
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // פלט ה-XDP השמור הוא UTF-8
finally
Pdf.Free;
end;
end;
רוטינת השמירה אז מבצעת commit לעריכה הממתינה, בודקת את התוצאה של SaveAs ומשווה את הערך שנפתח מחדש. TPdf.ClearFormFieldFocus מפזר את פוקוס הטופס, וזה הרגע שבו PDFium מבצעת commit לחוצץ העריכה של השדה הממוקד. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean ממלא את השדה הממוקד באופן תוכנתי, אבל הוא נשען על פוקוס שהעוטף עוקב אחריו דרך FocusFormField, שהולך על הערות widget. לעמוד dynamic XFA אין בדרך כלל כאלה, ולכן שם הטקסט מגיע בדרך כלל דרך קלט מקלדת ב-TPdfView, והפונקציה מחזירה False כשאין שדה נעקב בפוקוס
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// מילוי מתוסריט אופציונלי; False פירושו שאף שדה נעקב אינו בפוקוס
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // מבצע commit לחוצץ העריכה
if not Pdf.SaveAs(FileName) then // כולל את ה-flush הסופי (v3.125.2 ומעלה)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
התייחסו לבדיקת המחרוזת המשנה בתור בדיקת עשן. אלמנט ריק עשוי להיסדר בתור <Tag/>, תכונות יכולות להופיע על אלמנטי נתונים, וה-escaping מעבר ל-& ול-< הוא בחירה של המסדר. לבדיקות ייצור, טוענים את ה-XML שנפתח מחדש עם מפרסר XML אמיתי ומשווים את צומת הטקסט של אלמנט הנתונים הקשור. מריצים את הבדיקה גם פעמיים ברצף, כי פגם ה-newline הציג את צורתו המלאה רק בדור השני
עזר זריז: צ'קליסט נאמנות שמירת XFA
- פורסים את
pdfium.v8.dllמ-v3.125.2 ומעלה עבור טפסי XFA, ואת v3.125.3 ומעלה עבור ה-pdfium.dllהרגילה, כדי שתיקון הכתיבה הסופית יהיה בשניהם - מכוונים את
LibraryNameאל נתיב מלא ומגדיריםEnableV8Engineל-True; נתיב חסר נכשל במקום לטעון עותק אחר - מאשרים
TPdf.XFAו-TPdf.XfaRuntimeAvailableאחרי פתיחת המסמך - קוראים ל-
ClearFormFieldFocusלפניSaveAsכדי שהשדה הממוקד יבוצע - לעולם לא מתעלמים מהתוצאה ה-Boolean של
SaveAs; תוצאה שווא משאירה את הקובץ הקודם במקומו - מאמתים על ידי פתיחה מחדש ב-
TPdfחדש וקריאתGetXfaDatasets, עם fallback אלGetXfaFormPacketsעבור XFA של stream בודד - בודקים עם ערכים ריקים, רווחים פותחים, טקסט מרובה שורות,
&ותו מהמישור המשלים, על פני שני דורות שמירה - מצפים לכשלי שמירה מפורשים עבור DTDs, XMLDSig והערות בתוך החבילות החיות של XFA של stream בודד
- אם טופס דינמי מאבד גיאומטריית זמן ריצה בפתיחה מחדש, בודקים את ה-subform השורשי עבור
restoreState="auto"לפני שחושדים בספרייה
עבור מבנה ה-callbacks שה-rntime של ה-XFA מצפה לו מאפליקציית מארח, ראו את FPDF_FORMFILLINFO גרסה 2 וה-ABI של XFA ב-Delphi. ה-rntime של ה-V8, העוטף ל-Delphi ול-C++Builder ובקרת הצפייה כולם חלק מ-PDFium Component ל-Delphi ול-C++Builder, שכוללת את שני ה-runtimes ל-Windows עבור Win32 ו-Win64