תיבות סימון וכפתורי רדיו משוטחים כלא-מסומנים כי מצב המראה /AS מעולם לא סונכרן עם ערך השדה /V. PDFium Component, רכיב ה-VCL וה-LCL המבוסס-PDFium עבור Delphi, C++Builder, ו-Lazarus, קורא עכשיו את הערך הזה עם FPDFAnnot_GetFormFieldValue, שפותר את מילון השדה ההורה במקום את הערת ה-widget
דוח הבאג שהוביל לכאן הוא מהסוג שחושדים בו תחילה. לקוח משטח טופס הסכמה חתום, פותח את התוצאה, וכל תיבת סימון ריקה. פותחים את קובץ המקור ב-Acrobat והתיבות מסומנות בבירור. קוראים את קובץ המקור בחזרה דרך אותו רכיב וערכי השדה נכונים. רק הפלט המשוטח מאבד אותם, ורק עבור תיבות סימון וכפתורי רדיו: שדות טקסט באותו עמוד יוצאים בסדר
מדוע תיבות סימון לא-מסומנות אחרי שיטוח?
כי שיטוח לעולם לא מסתכל על /V. FPDFPage_Flatten אופה את זרם המראה של ה-widget לתוך תוכן העמוד, והמראה שהוא בוחר הוא זה שנקרא על ידי /AS. אם /AS עדיין אומר /Off בעוד ערך השדה אומר שהתיבה מסומנת, שיטוח בנאמנות אופה את המראה הכבוי. הערך מעולם לא אבד; הוא מעולם לא נבדק
ISO 32000-1 §12.5.5 מגדיר את מילון המראה /AP עם שלוש רשומות אפשריות, /N, /R, ו-/D. עבור תיבת סימון או כפתור רדיו, הרשומה /N אינה stream אלא תת-מילון שהמפתחות שלו הם שמות מצב מראה, ו-§12.5.2 הופך את /AS לבורר הנדרש כש-/N הוא תת-מילון. כך שתיבת סימון נושאת שני מראות בנויים-מראש ומצביע אחד. תפספסו את המצביע והרינדור שגוי בדרך שאף כמות של /V נכון לא תתקן. זו גם הסיבה שמצב הכשל שונה משדות טקסט, שאין להם מראה בנוי-מראש לבחור כלל: /N של שדה טקסט הוא stream בודד שחייב להיווצר מחדש מאפס אחרי שהערך משתנה, כך ש-GenerateFormAppearances מטפל בשני המקרים דרך נתיבי קוד נפרדים לגמרי ורק נתיב הכפתור היה שבור
איפה ערך תיבת הסימון באמת חי?
על מילון השדה, לא על ה-widget. ISO 32000-1 §12.7.5.2 מתאר תיבות סימון וכפתורי רדיו כשדות כפתור שה-/V שלהם הוא אובייקט שם שנוקב את מצב המראה הנוכחי, ו-§12.7.3.1 שם את /V בין הרשומות המשותפות לכל מילוני השדה. הערת ה-widget המוגדרת ב-§12.5.6.19 תורמת /AS ו-/AP. שום דבר במפרט לא מחייב widget לשאת /V
// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off
{ What the two objects look like when the field has several widgets:
12 0 obj % field dictionary (the parent)
<< /FT /Btn /T (Consent) /V /On
/Kids [ 13 0 R 14 0 R ] >>
endobj
13 0 obj % widget annotation (a kid)
<< /Type /Annot /Subtype /Widget /Parent 12 0 R
/AS /Off
/AP << /N << /On 20 0 R /Off 21 0 R >> >> >>
endobj }
FPDFAnnot_GetStringValue אינה פגומה. החוזה שלה בדיוק מה שהשם שלה אומר: מבאים רשומת מחרוזת ממילון ההערה שמסרתם לה. שאילת /V על אובייקט 13 מחזירה לא כלום כי לאובייקט 13 באמת אין /V. הפגם היה בקורא, שהניח מודל אובייקט שטוח ש-ISO 32000-1 לעולם לא הבטיח
מתי שדה ו-widget חולקים מילון אחד?
בכל פעם ששדה יש לו בדיוק widget אחד. §12.5.6.19 מתיר למילון השדה ולהערת ה-widget היחידה שלו להתמזג לתוך אובייקט אחד, ורוב כלי היצירה נוקטים את הקיצור הזה. באובייקט ממוזג /FT, /T, /V, /AS, ו-/AP כולם יושבים זה לצד זה, כך שקריאה ברמת-widget של /V מצליחה וכל הבאג נשאר בלתי-נראה
ברגע ששדה מחזיק שני widgets או יותר, המיזוג בלתי אפשרי, ו-§12.7.3.1 דורש שהwidgets יהפכו ל-/Kids של מילון שדה נפרד. כל קבוצת רדיו נמצאת בצורה הזו מבניה. כך גם תיבות סימון הסכמה שחוזרות בכותרת עליונה ותחתונה, וכל שדה שכלי יצירה העתיק לעמוד שני. זה כל ההסבר לכך שהפגם שרד חבילת רגרסיה: קורפוס הבדיקות היה מלא בטפסים בעלי-widget-יחיד וקבצי הלקוח לא היו כאלה. אם אתם עוברים על widgets בעצמכם במקום להסתמך על הרכיב, אותה חוסר-סימטריה מופיעה בסדר הספירה, וההערות על ניווט שדות טופס PDF עם PDFium Component מכסות איך מעבר הערות ברמת-עמוד קשור לעץ השדות ברמת-מסמך
קריאת הערך בדרך ש-PDFium מתכוון אליה
FPDFAnnot_GetFormFieldValue הוא ה-API הנכון, והוא היה מחווט ברכיב במשך זמן מה בלי שנתיב תיבת הסימון השתמש בו. הוא לוקח את הידית (handle) הטופס בתוספת ההערה, שזה האות שחשוב: עם סביבת מילוי-טופס זמינה, PDFium פותר את ההערה לפקד הטופס שלה וקורא את הערך מאובייקט השדה, כך שהוא מחזיר את התשובה הנכונה עבור פריסות ממוזגות ומפוצלות כאחד
FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
begin
// /AP is prebuilt per state; only /AS has to be synchronised with /V.
// FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
// which is where ISO 32000-1 12.7.5.2 keeps the value.
buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
if buflen >= 4 then
begin
SetLength(OrigVal, buflen div 2 - 1);
FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
end;
end;
שני פרטים בקטע הזה קל לטעות בהם. האורך המוחזר הוא ספירת בייטים עבור טקסט UTF-16 כולל הסופית, כך שספירת התווים היא buflen div 2 - 1 וערך של 2 אומר מחרוזת ריקה. השומר buflen >= 4 לכן אומר לפחות תו אמיתי אחד, וזה מה ששומר על שדה ללא /V כלל מלקבל את ה-/AS שלו נדרס בשם ריק
על מה /AS ו-/AP /N באמת מסכימים
הם מסכימים על שם, והשם נבחר על ידי מי שהפיק את הקובץ. §12.7.5.2 דורש שהמצב הכבוי ייקרא /Off, ומשאיר את המצב הדולק לגמרי ליצרן. /Yes הוא מוסכמה, לא כלל. Acrobat כותב /Yes, אך הרבה מחוללים כותבים /On, /1, /Choice1, או מילה מקומית, וקבוצת רדיו בדרך כלל נותנת לכל ילד שם מצב-דולק ייחודי כך שהקבוצה יכולה לבטא איזה כפתור נבחר. זו בדיוק הסיבה שהעתקת /V מילה-במילה לתוך /AS היא הפעולה הנכונה ולא hack: עבור פקד מסומן PDFium מדווח את שם המצב-הדולק שהקובץ עצמו מגדיר, ועבור לא-מסומן הוא מדווח Off, כך שהערך שכותבים לתוך /AS מובטח להיות מפתח שקיים בתת-המילון /AP /N של אותו widget. קידוד קשיח של /Yes היה עובד על פלט Acrobat ושובר בשקט בכל מקום אחר
סדר הפעולות, ואיפה עדיין נדרשת זהירות
הרצף קבוע ובלתי-סלחני: מפעילים מילוי טופס, מקצים ערכים, יוצרים מחדש מראות, משטחים, ואז שומרים. מדלגים על שלב היצירה-מחדש ו-FPDFPage_Flatten מוצא זרמי מראה ריקים או מיושנים ואופה אותם בלי תלונה, שזה אובדן נתונים שקט במקום החזרת שגיאה
Pdf.FileName := FormPath;
Pdf.FormFill := True; // required: FormHandle must exist
Pdf.Active := True;
Pdf.FormField[0] := 'On'; // writes /V only
Pdf.GenerateFormAppearances; // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
Pdf.SaveAs('consent-flat.pdf');
שני גבולות כנים נשארים. ראשית, הסנכרון כותב את ערך השדה לתוך ה-/AS של כל widget של אותו שדה, שנכון עבור תיבות סימון אך משוער עבור קבוצות רדיו שכל ילד שלהן מגדיר שם מצב-דולק משלו; ילד שה-/AP /N שלו אין לו רשומה שתואמת ל-/AS שנכתב אין לו מראה לבחור תחת §12.5.5, כך שכפתור לא-נבחר יכול להשתטח ללא כלום במקום עיגול ריק. ביקורת קבוצת רדיו עם FPDFAnnot_GetFormControlIndex לפני שיטוח שווה את השורות הבודדות. שנית, שום דבר מזה לא חל על XFA, שם הערך חי בחבילת נתוני XML במקום במילוני AcroForm, הפרדה מכוסה בהערות על עריכות שדה XFA שלא נשמרות. השיעור הכללי שווה שמירה מעבר לתיקון הספציפי הזה: בכל פעם ש-API לוקח את ידית הטופס בתוספת ההערה, הוא אומר לכם שהוא יפתור את היררכיית השדות עבורכם, ובכל פעם שהוא לוקח רק את ההערה הוא יקרא בדיוק את האובייקט שמסרתם. ההבחנה הזו גם שולטת בהחלפת נתונים, מכיוון שייצוא וייבוא נתוני טופס XFDF עובד בשמות שדה מלאים-מוסמכים, לעולם לא במיקומי widget
שיטוח טופס הוא אחת מהתכונות האלה שנראית כמו קריאת API בודדת ומתבררת כחוזה בין שלושה מילונים. אם הייתם מעדיפים לעבוד מול רכיב שכבר מקודד את החוזה הזה, PDFium Component עבור Delphi ו-C++Builder מגיע עם היצירה-מחדש של המראה, השיטוח, וגישת שדה הטופס המתוארים כאן כ-properties ומתודות רגילות