מאמר טכני

ערכי שדות AcroForm בירושה ואיפוסים בדלפי

רכיב HotPDF ל-Delphi מתייחס אל /FT, /Ff, /V ו-/DV על שדה AcroForm טעון כתכונות עוברות בירושה, שנפתרות בהליכה בשרשרת ה-/Parent. מאז v2.754.3 ו-v2.754.4, ילד בעל שם שהסוג שלו בא מההורה נשאר נגיש בנפרד, RemoveFormField משאירה את האחים שלו לבד, ו-ResetLoadedFormField מעתיקה את ברירת המחדל היורשת עם סוג אובייקט ה-PDF המקורי שלה. לפני זה, מספר מפתיע של טפסים רגילים נקראו לא נכון

הטופס שחושף את כל זה אינו אקזוטי. כלי כתיבה בונה צומת קבוצה בשם group שנושא /FT /Ch, את דגלי השדה ואת רשימת האפשרויות פעם אחת, ותולה תחתיו שני ילדים בעלי שם, a ו-b, שכל אחד מהם מילון ממוזג של שדה-plus-widget עם שום דבר מלבד /T, /Parent, /Rect וה-/V של עצמו. זו דרך לגיטימית לגמרי לשתף תכונות, והיא בדיוק המקרה שסעיף המגבלות של קביעת ערכי שדות טופס ב-PDF טעון עם דלפי סימן כלא מטופל: התאמת הכפתורים הביטה רק ב-/FT המקומי. המאמר הזה ממשיך מהמקום שבו הוא עצר, ומכסה איך עץ השדות מסווג, איך ערכים יורשים נקראים, ומה מותר לאיפוס שדה יחיד לכתוב

אילו רשומות AcroForm שדה יכול לרשת מההורה שלו?

ISO 32000-1 §12.7.3.1, טבלה 220, מסמן את /FT, /Ff, /V ו-/DV כעוברים בירושה, וטבלה 229 ב-§12.7.4.3 עושה זאת עבור /MaxLen של שדה טקסט, כך שכל קורא שמביט רק במילון המקומי ידווח סוג שגוי, דגלים שגויים וערך ריק עבור ילד תקין לגמרי. HotPDF מעבירה את כל הקריאות האלה דרך resolver פנימי אחד, HPDFLoadedInheritedFieldObject, שבודק במילון את המפתח, פותר הפניה עקיפה אם הוא מוצא אחת, ואחרת עוקב אחר /Parent עד 128 רמות לכל היותר, כי קבצים פגומים יכולים לבנות מעגלי /Parent שאין להם שום קשר ל-/Kids. ה-getters הציבוריים יושבים מעליו: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue ו-helpers האפשרויות GetLoadedFormFieldOptionCount ו-GetLoadedFormFieldOptions, שקולפים גם מערך /Opt המאוחסן על ההורה. כלל אחד ב-resolver קל להתבלבל בו: ההליכה עוצרת במילון הראשון שמכיל את המפתח, גם אם הערך שם הוא מחרוזת ריקה. /V () מקומי הוא דריסה מכוונת שמסתירה את ההורה, לא פער שיש למלא מגבוה יותר בעץ

דיאגרמת תכונות AcroForm יורשות של HotPDF: צומת קבוצה נושא /FT, /Ff ו-/Opt פעם אחת בזמן שהילדים בעלי השם group.a ו-group.b מחזיקים רק /T, /Parent, /Rect ו-/V מקומי, ומוצג HPDFLoadedInheritedFieldObject הולך ב-/Parent עד 128 רמות שבהן המילון הראשון שמחזיק מפתח מנצח וערך מקומי ריק מסתיר את ההורה
HotPDF פותרת את /FT, /Ff, /V, /DV ו-/Opt דרך resolver אחד שהולך בהורים, כך שילד בעל שם נשאר נגיש בזמן שערך מקומי ריק דורס במכוון את כל מה שהקבוצה שמעליו נושאת
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // ה-'group' נושא /FT /Ch, /Ff 131078 ו-/Opt; הילד
    // 'group.b' מחזיק רק /T, /Parent, /Rect ואת ה-/V של עצמו
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (ביט 18) + NoExport (ביט 3) + Required (ביט 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // ה-/V המקומי
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

למה /FT מקומי הוא הבדיקה השגויה לשדה קצה?

כי הורה יכול לספק את הסוג ועדיין להחזיק שדות ילד בעלי שם, ולכן נוכחות של /FT לא אומרת דבר על איפה עץ השדות נגמר. המעבר הישן הכריז על צומת כקצה בכל פעם שהיה לו /FT משלו או שאין לו /Kids. בטופס שלמעלה, ל-group יש גם /FT /Ch וגם /Kids, ולכן הוא נרשם כשדה אחד בשם group עם שני widgets, והשמות המאותה נקודה במלואם group.a ו-group.b פשוט נעלמו. GetFormFieldCount החזיר 1, חיפוש לפי שם ילד נכשל, ו-SetFormFieldValue יכלה לכתוב רק את ההורה המשותף. הבדיקה החלופית, HPDFLoadedFieldHasChildFields, מביטה בילדים במקום בהורה: ילד הוא שדה ילד אם יש לו /T משלו, יש לו /Kids משל עצמו, או שהוא בכלל לא מילון /Subtype /Widget. רק כשאף ילד לא עומד בתנאי הצומת הוא קצה, והילדים שלו מתייחסים כהערות ה-widget שלו

שני מקרי הקצה שעיצבו את הכלל הזה באים שניהם ממילונים ממוזגים, ש-§12.7.3.1 מתיר כשלשדה יש widget יחיד. מילון ממוזג בעל שם נושא /Subtype /Widget ועדיין הוא שדה ילד, ולכן ה-subtype לבדו לא יכול לשלוח אותו אל רשימת ה-widgets האנונימיים של ההורה; ה-/T מנצח. ההפך גם קורה: יש מפיקים שחוזרים על ה-/FT של ההורה על כל widget אנונימי, ולכן גם ל-/FT אין לגיטימציה כראיה ש-widget פותח שדה חדש. הסיווג משותף ל-cache היחסים, ל-FormFieldExists ול-RemoveFormField, וכל אחת מההליכות האלה מקליטה עכשיו את המילונים שכבר ביקרה בהם ועוצרת מעבר ל-128 רמות. קובץ רגרסיה שהקבוצה שלו מפרטת את עצמה פעמיים, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], עדיין מדווח בדיוק שני שדות במקום להתקיים לנצח או לספור את אותו צומת פעמיים

איך RemoveFormField נמנעת ממחיקת שדות אחים?

RemoveFormField מוחקת עכשיו רק את הילד שאתם מציינים, כי גילוי ומחיקה לבסוף מסכימים מהו שדה קצה. ההסכמה הזאת חשובה יותר משנראה. ה-overload לפי שם פותר אינדקס דרך cache היחסים ואז סופר שדות קצה בהליכה שנייה על /AcroForm /Fields. ברגע שה-cache תוקן כדי לראות את group.a ו-group.b, הליכת מחיקה לא מתוקנת עדיין הייתה מתייחסת אל group כשדה קצה יחיד, ואינדקס 0 היה מסיר את ההורה יחד עם כל אח ואח ואת כל ה-widgets שלהם. הליכת המחיקה משתמשת עכשיו באותה בדיקת HPDFLoadedFieldHasChildFields ובאותה קבוצת ביקורים, אוספת את הערות ה-widget של הילד שהוסר בלבד, מסירה אותן מה-/Annots של כל עמוד, ומסירה את ההורה רק כשמערך ה-/Kids שלו נגמר ריק. הרגרסיה בודקת את שלושת המקומות שבהם טעות תגלה: ה-/Kids של ההורה, ה-/Annots של העמוד, והערך והמראה של האח ששרד — גם אחרי שכתוב מלא וגם אחרי עדכון מצטבר

דיאגרמת הישרדות אחים של RemoveFormField ב-HotPDF: הליכת המחיקה עושה שימוש חוזר ב-HPDFLoadedFieldHasChildFields ובקבוצת הביקורים מהגילוי, מסירה רק את הילד הנקוב group.a מה-/Fields של AcroForm ומה-/Annots של העמוד, ומשאירה את ההורה המשותף בזמן שמערך ה-/Kids שלו עדיין מחזיק את group.b ששרד
גילוי ומחיקה לבסוף מסכימים מהו שדה קצה, ולכן הסרת ילד נקוב אחד משאירה את הערך והמראה של האח שלו שלמים גם אחרי שכתוב מלא וגם אחרי עדכון מצטבר
// הסרת ילד נקוב אחד; האח שלו וההורה המשותף שורדים
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// הסוג, הדגלים והאפשרויות עדיין נפתרים דרך ההורה
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

מה ResetLoadedFormField כותבת כשברירת המחדל עוברת בירושה?

ResetLoadedFormField כותבת /V מקומי שהוא עותק טרי של ה-/DV היורש עם אותו סוג אובייקט PDF, והיא מאמתת את ברירת המחדל כולה לפני שהיא נוגעת בשדה. סוג האובייקט חשוב כי ה-getters הסקלריים משטחים הכול לטקסט. ברירת מחדל של תיבת סימון היא שם כמו /Yes, ברירת מחדל של list box רב-בחירה היא מערך של מחרוזות, וברירת מחדל של טקסט עשויה להיות מחרוזת UTF-16 הקסדצימלית; העתקה של אחד מהם דרך GetLoadedFormFieldDefaultValue תהפוך את השם למחרוזת, את המערך למחרוזת ריקה ואת המחרוזת ההקסדצימלית לספרותיה המילוליות. האיפוס לכן מסתעף על הסוג היורש: שדות טקסט ובחירה מקבלים אובייקט מחרוזת חדש ששומר על דגל IsHexadecimal, שדות בחירה עם ברירת מחדל של מערך מקבלים מערך חדש של מחרוזות חדשות, וכפתורים שאינם pushbutton מקבלים אובייקט שם חדש. להעתיק, ולא להצביע על אובייקטי ההורה, זו בחירה מכוונת: /V ששיתף את מערך ה-/DV של ההורה או את מספר האובייקט שלו היה משנה את ברירת המחדל בפעם הבאה שמישהו יערוך את הערך. ברירת מחדל מסוג שגוי, או מערך בחירה שמכיל משהו שאינו מחרוזות, מעלה חריגה ומשאיר את /V ואת /I בדיוק כפי שהיו. Pushbuttons, שאין להם ערך (טבלה 226, ביט 17), ושדות חתימה חוזרים לנתיב המחרוזת-בלבד הישן

דיאגרמת איפוס מוטיפוס של HotPDF: ResetLoadedFormField מסתעפת על סוג אובייקט ה-/DV היורש, כותבת אובייקט שם טרי עבור תיבת סימון, מערך חדש של מחרוזות חדשות עבור בחירה רב-בחירה, מחרוזת ששומרת על IsHexadecimal עבור טקסט הקסדצימלי, מחרוזת ריקה או /Off כשאין /DV, ומעלה חריגה בלי לגעת ב-/V או ב-/I על אי-התאמת סוג
להעתיק במקום להצביע על אובייקטי ההורה מונע מעריכת ערך מאוחרת לשנות בשקט את ברירת המחדל, ו-pushbuttons ושדות חתימה חוזרים לנתיב המחרוזת-בלבד הישן

כשאין /DV בשום מקום למעלה בשרשרת, המתודה שומרת על החוזה שלה לניקוי וכותבת מחרוזת ריקה מקומית, או /Off עבור שדה תיבת סימון או רדיו. למחוק את ה-/V המקומי היה נראה מסודר יותר ויהיה שגוי: ההורה עשוי להחזיק ערך נוכחי, והסרת הדריסה של הילד תחזיר את הערך הזה בשקט. זו גם הסיבה שאיפוס שדה יחיד אינו פעולת ה-ResetForm של §12.7.5.3, שמציג מריץ על קבוצת שדות כשהמשתמש לוחץ על כפתור, כמתואר בבניית שדות AcroForm ופעולות עם HotPDF. ResetLoadedFormField היא פעולת עריכה על שדה טעון אחד, עם הכלל משלה למקרה חסר-ברירת-מחדל, והיא מקליטה את השדה דרך NoteLoadedFormFieldDirty כך שחישוב מחדש מצטבר יראה את השינוי

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // ההורה מחזיק /DV [(b) (r)] על list box MultiSelect: ל-group.a נכתב
    // ה-/V [(b) (r)] משלו ו-/I [0 2] טרי; להורה לא נוגעים
    Pdf.ResetLoadedFormField(Field.Index);
    // ה-getters הסקלריים אינם יכולים לייצג את ברירת המחדל של המערך
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // ריק
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

לשמור על /V, /I ו-/AS מסונכרנים

איפוס נכון רק אם אינדקס הבחירה ומצב המראה הולכים אחרי הערך, ולכן ResetLoadedFormField מסיימת עם אותם שני מיישבים כמו SetFormFieldValue. ה-HPDFReconcileChoiceSelection מקבל עכשיו ערך מערך: הוא מוחק את ה-/I המקומי בלי לשנות אותו, משווה כל ערך מול חצי ה-export של כל רשומת /Opt, וכותב /I ממוין חדש אחד, כך שאיפוס אל [(b) (r)] מול האפשרויות b, g, r מניב /I [0 2]. ה-ReconcileLoadedButtonAppearanceStates מבקש עכשיו את הסוג היורש, כך שתיבת סימון ילד שה-/FT /Btn שלה יושב על ההורה לבסוף מקבלת את ה-/AS שלה מוגדר. בצד הכתיבה, SetFormFieldValue ו-SetLoadedFormFieldDefaultValue מאחסנות אובייקט שם עבור כפתור לא-pushbutton יורש גם כשלילד אין רשומה מקומית שממנה ניתן להעתיק את הסוג. וכשה-EnsureLoadedFieldAppearanceStream בונה מחדש מראות כפתור, הוא כותב /AS /Off אלא אם הערך תואם את מצב ה-on, ונותן לכל זרם מצב /Type /XObject, /Subtype /Form ו-/BBox ראויים; לפני v2.754.4, יצירת המראה מחדש אחרי איפוס יכלה לסמן את התיבה שוב לפני שהקובץ נשמר

מגבלות ששווה להכיר לפני שבונים על זה

ה-getters הסקלריים נשארים סקלריים. GetFormFieldValue ו-GetLoadedFormFieldDefaultValue מחזירים מחרוזת ריקה עבור ערך מערך, ממחרזים מספרים ובוליאניים כ-42 או true, ומדווחים מחרוזת מקודדת-hex בכתיב ההקסדצימלי שלה. מעגל /Parent מסיים את ההליכה בלי חריגה, כך ששדה שהסוג שלו אבד במעגל מדווח lfftUnknown ודגלים 0 במקום להיכשל. SetFormFieldValue ו-ResetLoadedFormField תמיד כותבות את הילד שאליו פניתם ולעולם לא מקדמות ערך אל ההורה המשותף, מה שנכון עבור ילדים עצמאיים אבל אומר שקבוצות רדיו פונים אליהן דרך השדה שמחזיק את הבחירה. וכל קריאה מבצעת commit של שדה אחד בנפרד; שום דבר כאן לא הופך קבוצת איפוסים לטרנזקציה

פתרון התכונות היורשות, סיווג עץ השדות המאוחד והאיפוס המוטיפוס שמתוארים כאן הם חלק מה-API של טפסים טעונים ברכיב HotPDF ל-Delphi עבור Delphi ו-C++Builder, לצד יצירת השדות המכוסה בהוספת שדות AcroForm ל-PDF טעון בדלפי