רכיב HotPDF ל-Delphi ממלא שדה AcroForm קיים ב-PDF טעון דרך THotPDF.SetFormFieldValue, בכתובת שהיא או אינדקס שדה מבוסס-0 או שם שדה מלא. כתיבת רשומת ה-/V החדשה היא החלק הקל; מה שהופך את הקריאה לאמינה על טפסים מהעולם האמיתי הוא שאותה מתודה גם שומרת על עקביותם של שלושה חלקי מצב שנעלמים מהעין עד שהם משתבשים: הזהות המפוענחת של השדה כך שאפשר למצוא שם שאינו ASCII בכלל, מצב המראה /AS ב-widgets של תיבות סימון וכפתורי רדיו, ומערך אינדקסי הבחירה /I בשדות בחירה. זרם המראה הנראה הוא צעד נפרד ומפורש דרך EnsureLoadedFieldAppearanceStream
התרחיש הוא היומיומי: לקוח שולח לכם טופס שלו, הצהרת מס, תביעת ביטוח, הזמנת רכש שמישהו בנה ב-Acrobat לפני שנים, והאפליקציה שלכם בדלפי צריכה למלא אותו ממסד נתונים ולהחזיר קובץ שנפתח נכון בכל מקום. אין לכם שליטה על איך הטופס נכתב. שמות שדות עשויים להיות מקודדים ב-UTF-16, ערכי ה-export של תיבות סימון עשויים להיות 2 ולא Yes, ותיבות combo עשויות להשתמש בזוגות אפשרויות [export display]. לכל אחד מהפרטים האלה יש כלל ב-ISO 32000-1, וכל כלל הוא משהו ש-SetFormFieldValue מטפלת בו כעת בשבילכם. המאמר הזה עוסק במה שהיא עושה, למה, ואיפה היא נעצרת. לבעיה האחות של יצירת שדות שעדיין לא קיימים, ראו הוספת שדות AcroForm ל-PDF טעון בדלפי
למה SetFormFieldValue לא מוצאת שדה עם שם שאינו ASCII?
לפני v2.752.1 התשובה הייתה קידוד: השדה חי בקובץ תחת שם UTF-16BE הקסדצימלי, ו-cache השמות שמר את כתיב ה-hex במקום את הטקסט. ISO 32000-1 §12.7.3.1 מגדיר את שם השדה החלקי /T כמחרוזת טקסט, ו-§7.9.2.2 אומר שמחרוזת טקסט יכולה להיות UTF-16BE עם סימן סדר בתים FE FF מוביל. כלי כתיבה מסריאלים שמות כאלה כדבר שבשגרה כמחרוזות hex לפי §7.3.4.3, כך ששדה בשם Straße מגיע בתור <FEFF005300740072006100DF0065>. בתוך HotPDF, THPDFStringObject.Value מחזיק את הטקסט ההקסדצימלי הגולמי בכל פעם ש-IsHexadecimal דלוק, וזה בדיוק מה שאתם רוצים למחזור חסר אובדן של המילון המקורי ובדיוק מה שאתם לא רוצים כמפתח חיפוש. HPDFLoadedFormTextName מפרידה בין שני העניינים. כשבניית cache היחסים רצה, כל ערך /T עובר דרכה: אם אובייקט המחרוזת הקסדצימלי, HPDFHexToBytes משחזרת את רצף הבתים; אם הבתים מתחילים ב-FE FF ובאורך זוגי, המטען מפוענח כ-UTF-16BE ומקודד מחדש כ-UTF-8; התוצאה מצורפת אז לשם ההורה שלה עם נקודה כדי ליצור את השם המלא ש-§12.7.3.1 מתארת, כך שילד בשם City תחת הורה בשם Address נרשם בתור Address.City. מפתח ה-cache מנורמל לאותיות קטנות, מה שהופך גם SetFormFieldValue('address.city', ...) להצלחה; זו נוחות מעבר לתקן, כי המפרט מתייחס לשמות כרגישים לאותיות רישיות. ומה שחשוב: רק מפתח ה-cache משתנה. אובייקט ה-/T במילון השדה שומר על הקידוד ההקסדצימלי שלו, כך ששמירת המסמך לא כותבת מחדש את הזהות של שדה שרק מילאתם
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// שמות מלאים מפוענחים ממחרוזות /T ב-UTF-16BE
// ומצורפים בנקודות, כך ששמות מקוננים ושאינם ASCII נפתרים
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// ערכים שאינם Latin-1 נוסעים כ-hex של UTF-16BE עם קידומת FEFF
// ונכתבים כמחרוזת הקסדצימלית של PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
מה SetFormFieldValue באמת כותבת?
שתי ה-overloads מריצות את אותם חמישה שלבים: לאתר את מילון השדה, לכתוב את /V דרך HPDFSetDictFormValue, ליישב את אינדקסי הבחירה, לסמן את המילון כמזוהם, ליישב את מצבי המראה של כפתורים, ולבסוף לרשום את אינדקס השדה דרך NoteLoadedFormFieldDirty. הצעד האחרון חשוב אם הטופס נושא סקריפטים של חישוב, כי קבוצת המזוהמים היא מה שה-overload חסר הפרמטרים RecalculateLoadedFormFieldsIncremental צורך כדי להריץ מחדש רק את החישובים שקוראים באופן טרנזיטיבי בשדה שהשתנה. HPDFSetDictFormValue עצמה זהירה לגבי סוג האובייקט שהיא מחליפה. אם ה-/V הקיים הוא אובייקט שם, שזה מה ששדות תיבת סימון ורדיו משתמשים בו כערך ה-export שלהם, הערך החדש נכתב כשם, ואף פעם לא כמחרוזת, כי שמות ב-PDF הם ASCII בלבד מעצם בנייתם. אחרת היא כותבת אובייקט מחרוזת ובוחנת את הערך שהעברתם: מחרוזת שמתחילה ב-FEFF, באורך זוגי, ומורכבת רק מספרות hex מטופלת כצורת ה-wire של UTF-16BE מ-§7.9.2.2 ונשמרת עם IsHexadecimal דלוק, כך שהיא מסריאלית כ-<FEFF...> ולא כ-(FEFF...) מילולי. זה המנגנון שעליו נשענת שורת ה-City שלמעלה; כל מחרוזת אחרת נשמרת כמחרוזת literal עם הבתים שנתתם, כך שלטקסט Latin פשוט אתם מעבירים טקסט פשוט
למה תיבת סימון שומרת על הסימון הישן אחרי שהערך משתנה?
כי בשדה כפתור הערך לבדו לא מכריע מה מצויר. ISO 32000-1 §12.7.4.2.3 מפרט ש-widget של תיבת סימון נושא מצב מראה /AS שמציין איזה זרם ב-/AP /N מוצג כעת, והמציגים מציירים מתוך /AS ולא מתוך /V. אם תשנו את /V ל-Yes אבל תשאירו את /AS על Off, הקובץ סותר את עצמו מבפנים, ושטיחה תאפה בשמחה את המראה הישן והלא מסומן לתוך העמוד בעוד נתוני הטופס אומרים מסומן. ReconcileLoadedButtonAppearanceStates קיימת כדי לסגור את הפער הזה: עבור שדה שה-/FT שלו הוא Btn, היא מבקרת במילון השדה עצמו ובכל רשומה במערך ה-/Kids שלו, קוראת את שם מצב ה-on מ-/AP /N, וכותבת מחדש את /AS לשם הזה כשהוא תואם את ערך השדה או ל-Off כשהוא לא
שני פרטים מטפסים אמיתיים עיצבו את התיקון ב-v2.752.3. ראשית, מילון מראה רגיל רשאי להכיל רק את מצב ה-on; §12.7.4.2.3 מכנה את מראה ה-off בשם Off אבל כלי כתיבה מרבים להשמיט את הזרם שלו ולתת למציג לא לצייר כלום. קוד קודם נשר כשהמילון החזיק פחות משתי רשומות, כך שתיבות הסימון בעלות מצב יחיד שמרו בשקט על הסימון הישן. הבדיקה היא כעת פשוט שהמילון אינו ריק, ושם מצב ה-on נלקח כמפתח הראשון שאינו Off. שנית, שם מצב ה-on הוא מה שהמחבר בחר. טפסים אמיתיים משתמשים ב-2, Yes, On או מילה מתורגמת, כך שההשוואה היא מול המפתח בפועל, בלי תלות באותיות רישיות, ואף פעם לא מול Yes קבוע בקוד. כפתורי רדיו מוסיפים קמט אחד נוסף, המתואר ב-§12.7.4.2.4: הבחירה חיה ב-/V על שדה ההורה, בעוד הילדים הבודדים מחזיקים בבעלותם את ה-widgets ובדרך כלל אין להם /V משלהם. ה-helper המקונן InheritedButtonValue עולה לכן בשרשרת ה-/Parent, עד 64 רמות, עד שהוא מוצא ערך שאינו ריק, כך שכל ילד מושווה לערך של הקבוצה שאליה הוא שייך. קביעת ההורה לערך ה-export של ילד אחד מדליקה בדיוק את הילד הזה ומכבה כל אח
// תיבת סימון: ערך ה-export חייב לתאום את מפתח מצב ה-on ב-/AP /N
// (לרוב 'Yes', אבל טפסים אמיתיים משתמשים ב-'2', 'On' או כל דבר אחר)
Pdf.SetFormFieldValue('Consent', 'Yes');
// קבוצת רדיו: ה-/V נכתב על ההורה; כל widget ילד מקבל
// /AS שמוגדר לשם ה-export שלו או ל-Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// ניקוי תיבת סימון: כל ערך שלא תואם שום מצב on מניב /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
שדות בחירה: לשמור על /I מסונכרן עם /V
בתיבת combo או list box, ה-/V אינו המקום היחיד שבו נרשמת בחירה. טבלה 231 ב-§12.7.4.4 מגדירה את /I כמערך של אינדקסים מבוססי-0 אל /Opt שמזהה את הפריטים הנבחרים, ומציג שמוצא את /I מצביע על אפשרות 0 בעוד /V מציין אפשרות 3 עשוי להאיר את השורה הלא נכונה. מאז v2.754.1, HPDFReconcileChoiceSelection רצה בתוך כל קריאה ל-SetFormFieldValue וכשה-/FT העובר בירושה הוא Ch, היא בונה מחדש את /I מהערך החדש. סדר הפעולות מכוון. רשומת ה-/I המקומית נמחקת ראשונה, בלי לגעת בתוכן שלה: אם המערך הישן היה אובייקט עקיף שמשותף עם שדה אחר, שינוי שלו במקום היה משחית את הבחירה של השדה האחר, ולכן השגרה מפילה את ההפניה ויוצרת מערך ישיר טרי במקומה. אחר כך היא פותרת את /Opt דרך שרשרת ה-/Parent, כי אפשרויות בחירה עשויות לעבור בירושה, וסורקת את הרשומות. אפשרות שהיא מחרוזת חשופה מושווית ישירות; זוג [export display] מושווה לפי איבר ה-export שלו, וזוג עם פחות משני איברים מדולג. שני הצדדים עוברים דרך HPDFLoadedFormTextName, כך שאפשרות hex ב-UTF-16 תואמת ערך hex ב-UTF-16 בלי שתאלצו לאיית אותם באופן זהה. בהתאמה הראשונה נכתב /I בן איבר אחד והסריקה נעצרת; ערך סקלרי תמיד מחליף כל בחירה מרובה קודמת, בלי קשר לדגל MultiSelect
כששום דבר לא תואם, לא נכתב /I בכלל. זו התוצאה הנכונה לתיבת combo ניתנת לעריכה, שבה §12.7.4.4 מתיר למשתמש להקליד ערך מחוץ לרשימת האפשרויות; לערך כזה אין אינדקס, ואינדקס מיושן יהיה גרוע מלא כלום. זו גם מה שמקבלים אם תעבירו תווית display במקום ערך export לרשימת אפשרויות מזווגת, ולכן כשקופסת combo מסרבת להציג את הבחירה שלכם, בדקו איזה חצי מהזוג סיפקתם
// /Opt הוא [[US United States] [CA Canada] [MX Mexico]]:
// ההשוואה לפי ערך ה-export, ו-/I הופך ל-[1]
Pdf.SetFormFieldValue('Country', 'CA');
// combo ניתן לעריכה עם ערך מחוץ ל-/Opt: ה-/V נכתב,
// ה-/I מוסר, ושום אינדקס לא מומצא
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
ערך ומראה הם שתי פעולות נפרדות
SetFormFieldValue אף פעם לא נוגעת בזרם המראה של שדה טקסט או בחירה. אחרי הקריאה, ה-/V מחזיק את הטקסט החדש בעוד /AP /N עדיין מצייר את הישן, ואיזה משניהם מציג מציג תלוי בשאלה אם מילון ה-AcroForm נושא /NeedAppearances true לפי §12.7.3.3 ואם המציג מכבד אותה. אם אתם צריכים שהקובץ יציג את הערך החדש בכל קורא, כולל משטחים ומחוללי תמונות ממוזערות שמתעלמים מהדגל, קראו ל-EnsureLoadedFieldAppearanceStream עם אינדקס השדה. היא בונה Form XObject ממחרוזת ה-/DA העוברת בירושה, מה-quadding ב-/Q, מפריסת ה-comb ב-/MaxLen ומהערך, פותרת את הפונט הנקוב דרך משאבי ה-/DR של ה-AcroForm כך שפונט Type0 שומר על פונט הצאצא שלו במקום להתדרדר ל-Helvetica, ומחזירה True כשקיבל widget אחד לפחות זרם. ה-overload לפי שם של SetFormFieldValue לא מחזיר לכם אינדקס, ולכן השיגו אחד דרך GetFormField, שמחזירה THPDFLoadedFormField בבעלותכם ושעליכם לשחרר. מערך הרגרסיה של השינוי ב-v2.752.1 מפורש לגבי הפיצול הזה: הוא קובע ערך, קורא ל-EnsureLoadedFieldAppearanceStream, ואז מרנדר את העמוד ובודק שהפיקסלים בתוך מלבן ה-widget השתנו בעוד הפיקסלים מחוצה לו לא. אימות שה-/V השתנה לא מוכיח דבר על מה שהמשתמש יראה
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// לצייר את הערך החדש אל /AP כדי שמציגים שמתעלמים
// מ-/NeedAppearances עדיין יציגו אותו
if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
raise Exception.Create('No widget rectangle to paint into');
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;
מגבלות ששווה להכיר לפני שאתם בונים על זה
ReconcileLoadedButtonAppearanceStates בודקת את ה-/FT המקומי של המילון שאליו פניתם, כך שהיא פועלת על הורה של רדיו או על תיבת סימון שנושאת /FT משלה; widget ילד שפונים אליו לבדו, כשה-/FT נמצא רק אצל ההורה שלו, לא מיושב בנתיב הזה. HPDFReconcileChoiceSelection מטפלת בערך סקלרי בודד וכותבת לכל היותר אינדקס אחד; תיבות list עם בחירה מרובה של כמה רשומות נבחרות נמצאות מחוץ למה ש-SetFormFieldValue מדגמנת. אף אחת מהשגרות לא מאמתת את הערך שהעברתם מול /Opt או מול מפתחות מצב ה-on, כך ששגיאת הקלדה מפיקה תיבת סימון Off או combo בלי אינדקס במקום חריגה. ו-GetFormFieldValue מחזירה את טקסט ה-/V המאוחסן כפי שהוא יושב במילון, מה שלערך מקודד hex משמעו הכתיב ההקסדצימלי ולא הטקסט המפוענח
אחרי שהערכים בפנים והמראות צבועות, שני הצעדים הטבעיים הבאים יושבים משני צדי הפעולה הזו. החלפת נתוני שדות עם מערכות חיצוניות בכמויות, ולא בקריאה אחת של SetFormFieldValue בכל פעם, היא מה שייבוא וייצוא XFDF בדלפי מכסה. וכשהטופס הממולא סופי ולא אמור להיות ניתן לעריכה יותר, שטיחת שדות AcroForm ו-XFA בדלפי אופה בדיוק את מצבי ה-/AS ואת זרמי המראה המתוארים כאן לתוך תוכן עמוד סטטי, וזו הסיבה שקביעתם כעקביים לפני השטיחה אינה אופציונלית
ה-API לעריכת טפסים טעונים במאמר הזה, כולל SetFormFieldValue, EnsureLoadedFieldAppearanceStream וגרף החישוב מחדש המצטבר, נשלח כחלק מרכיב HotPDF ל-Delphi עבור Delphi ו-C++Builder