מאמר טכני

הוספת שדות AcroForm ל-PDF טעון ב-Delphi

יש לך תבנית חשבונית של צד שלישי, או חוזה בארכיון שמישהו יצר לפני שנים בתוכנה שאי אפשר למצוא עוד, והדרישה היא להפוך אותו לאינטראקטיבי: להוסיף תיבת חתימה בפינה, לצרף כמה שדות טקסט, ולהפוך רשימת בדיקה שטוחה לתיבות סימון אמיתיות. העניין הוא שלא יצרת את ה-PDF הזה מאפס. הוא כבר קיים, יש בו כבר עמודים, זרמי תוכן וגופנים שאינם בשליטתך, ואתה צריך להשתיל עליו רכיבי AcroForm בלי לבנות הכול מחדש. זו בעיה אחרת לגמרי מאשר יצירת טופס במסמך חדש, והחלק שמכשיל אנשים הוא בלתי נראה עד שמספקים את התוצאה לקורא והצפייה מגלה שהשדות שכתבת אינם מופיעים בשום מקום בדף

HotPDF הוא רכיב PDF מקורי ל-VCL עבור Delphi ו-C++Builder, ומגרסה v2.247.0 הוא חושף משפחה ייעודית של מתודות בדיוק לזה: בניית כל ששת סוגי השדות הסטנדרטיים ישירות על מסמך שנטען באמצעות LoadFromFile. המאמר הזה עובר על מה שהמתודות האלה עושות, על מילון ISO 32000-1 שהן בונות, ועל הדגל האחד שבלעדיו כל התרגיל יוצר בשקט קובץ שנראה ריק

למה יצירת שדות במסמך טעון היא מסלול קוד נפרד

כשאתה בונה PDF מאפס, HotPDF שולט במודל האובייקטים כולו. כל עמוד הוא עטיפה ניתנת לכתיבה מסוג THPDFPage, והוספת שדה טקסט דרך AddTextField מחברת את הווידג'ט החדש לאובייקט ההערות של העמוד, לאובייקט העמוד, ולמכלול השדות של הטופס, ואז מייצרת זרם מראה מתוך משאבי הגופנים של המסמך. זרם המראה הוא המשטח הנראה של הווידג'ט, התיבה והמסגרת וכל טקסט ברירת המחדל, מצוירים כמפעילי ציור ב-PDF שהצופה מרנדר כלשונם

מסמך טעון לא מספק לך אף אחת מהתשתיות האלה. העמודים הגיעו כמילונים גולמיים; אין עטיפת THPDFPage ניתנת לכתיבה שאפשר לתלות עליה וידג'ט, וחשוב מכך אין צינור משאבי גופנים שמחכה לצייר זרמי מראה. לכן המסלול של מסמך טעון בוחר נתיב אחר. הוא כותב את מילוני השדה ישירות על גרף האובייקטים שנפרש, ומפנה לעמודים לפי אינדקס שמתחיל מאפס במקום לפי אובייקט עמוד. סוגי השדות וסיביות הדגלים תואמים בדיוק למסלול של יצירה מאפס, כך ששדה Text הוא שדה Text בכל מקרה; מה שמשתנה הוא האינסטלציה שמתחת, ובעיקר האופן שבו מצויר המשטח של הווידג'ט

דגל /NeedAppearances אינו אופציונלי כאן

זהו הנתון היחיד שקובע אם העבודה שלך תופיע. משום שהמסלול של מסמך טעון אינו מייצר זרמי מראה, וידג'ט שנוסף זה עתה מגיע לצופה בלי ערך /AP: שדה בלי משטח מתואר. הרבה צופים, כשמבקשים מהם לרנדר וידג'ט שאין לו מראה ואין להם הוראה לבנות אחד, מציירים כלום. השדה נמצא בקובץ, תקף מבחינה מבנית, ניתן לאיתור על ידי כלי מילוי טפסים, ולגמרי בלתי נראה לאדם

מסלול המילוט מוגדר ב-ISO 32000-1 §12.7.3: מילון AcroForm נושא בוליאני /NeedAppearances, וכאשר הוא true קורא תואם חייב לבנות בעצמו את זרמי המראה החסרים מתוך מחרוזת /DA ‏(מראה ברירת מחדל) של כל שדה ומתוך הערך שלו. HotPDF מגדיר זאת עבורך. בפעם הראשונה שבה מוסיפים שדה כלשהו למסמך טעון, EnsureLoadedAcroForm רץ: אם לקטלוג אין /AcroForm הוא יוצר אחד, אם אין מערך /Fields הוא יוצר גם אותו, והוא כופה /NeedAppearances true. אינך קורא לו ישירות, אבל הידיעה שהוא קיים מסבירה את ההתנהגות. היא גם מסבירה הסתייגות פרקטית שכדאי לומר במפורש: כמה צופים מינימליים או לא תואמים מתעלמים מ-/NeedAppearances ועדיין אינם מציגים כלום. עבור קוראים נפוצים הדגל עושה את עבודתו, אבל אם הקהל שלך משתמש במנוע רינדור מוטמע חריג, בדוק אותו שם לפני שאתה מבטיח משהו

הוספת ששת סוגי השדות

כל מתודה פועלת באותו מבנה. אתה מעביר את אינדקס העמוד שמתחיל מאפס, את ארבע הפינות של מלבן הווידג'ט בקואורדינטות מרחב המשתמש של PDF, את שם השדה, וכל ארגומנט נוסף שהסוג דורש. המלבן הוא X1, Y1, X2, Y2 כשהמקור של PDF נמצא בפינה השמאלית התחתונה של העמוד, כך שערכי Y גדולים יותר יושבים גבוה יותר; זו מוסכמת הקואורדינטות של פורמט הקובץ, לא של מסך שמבוסס על פינה שמאלית עליונה, ולטעות בזה זו הטעות השנייה בשכיחותה אחרי לשכוח את הדגל. כל קריאה מחזירה את אינדקס השדה החדש שמתחיל מאפס, או -1 אם אינדקס העמוד היה מחוץ לטווח או שלא ניתן היה לפתור את אובייקט העמוד

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

ארגומנטי המחרוזת השלישי והרביעי של שדה הטקסט הם שם השדה והערך ההתחלתי שלו ב-/V; הארגומנט המספרי הוא /MaxLen, והוא נכתב רק כאשר הוא גדול מאפס. HotPDF נותן לכל שדה שניתן לעריכה מחרוזת מראה ברירת מחדל של /Helv 12 Tf 0 0 0 rg, וזה מה שצופה שמכבד /NeedAppearances קורא כדי לקבוע באיזה גופן ובאיזה צבע הוא יצבע את הערך. תיבת הסימון מקבלת ערך ייצוא, כלומר המחרוזת שהטופס שולח כשהתיבה מסומנת, וגם ערך בוליאני למצב ההתחלתי; פנימית היא כותבת את רשומות השם המתאימות /V, /AS ו-/DV כך שמצב on/off נשאר עקבי ברגע שהקובץ נפתח. ערך ייצוא ריק מקבל כברירת מחדל את Yes, השם המקובל של המצב "מופעל" בתיבת סימון

שדות בחירה ודגלי הביט /Ff

ComboBox ו-ListBox הם שניהם שדות בחירה, מסוג /Ch ב-ISO 32000-1 §12.7.4. ההבדל בין תיבת בחירה נפתחת לבין רשימה נגללת הוא ביט אחד בערך הדגלים של השדה /Ff: ביט 18, דגל Combo, ערך $40000. HotPDF מגדיר את הביט הזה עבור AddLoadedComboBox ומשאיר אותו כבוי עבור AddLoadedListBox; מעבר לכך השניים זהים, ושניהם מקבלים את האפשרויות שלהם כמערך פתוח של מחרוזות שנכתב לרשומת /Opt

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

שתי הערות על רשימת האפשרויות. HotPDF כותב כל רשומת /Opt כמחרוזת פשוטה, שבה ערך הייצוא והתווית המוצגת הם אותו טקסט. ISO 32000-1 §12.7.4.4 מאפשר גם את הצורה הדו-רכיבית [export display] כאשר צריך שערך השליחה יהיה שונה ממה שהמשתמש רואה; מתודות היצירה למסמך טעון משתמשות בצורה הפשוטה של מחרוזת אחת, כך שאם אתה צריך ערכי ייצוא ותצוגה שונים תגדיר אותם בעצמך על המילון המתקבל. והערך שאתה מעביר כבחירה הנוכחית של השדה צריך להיות אחת מהאפשרויות שסיפקת, מפני שהצופה מתאים אותו לרשימה

הכפתור הדוחף הוא המקרה הנוסף שמונע על ידי דגלים: סוג השדה /Btn עם ביט 17, דגל PushButton, ערך $10000. הביט הזה הוא שמבדיל בין כפתור לחיץ לבין תיבת סימון, שגם היא שדה /Btn אבל בלעדיו. הכיתוב שאתה מעביר נכתב אל מילון מאפייני המראה /MK ככיתוב הרגיל /CA. חשוב להיות כנים לגבי ההיקף כאן: הכפתור נוצר עם התווית והמלבן שלו, אבל מתודת היצירה למסמך טעון אינה מצרפת פעולה, כך שמבחינת עצמו זהו כפתור שנראה נכון ולא עושה דבר בלחיצה. חיבור פעולות שליחה, איפוס או JavaScript הוא עניין נפרד; בצד של יצירה מאפס, זרימת העבודה של שדה יחד עם פעולה מכוסה ב-בניית שדות AcroForm ופעולות ב-Delphi, וזה נקודת ההשוואה הנכונה למה שהמסלול הטעון משאיר במכוון בחוץ

המילון שכל שדה חולק

מתחת לכל שש המתודות יש בונה משותף אחד שמרכיב את הערת הווידג'ט ומרשום אותה בשני מקומות. הוא כותב /Type /Annot ו-/Subtype /Widget, את מערך /Rect מארבע הקואורדינטות שלך, את דגלי ההערה /F 4 שמגדירים את דגל ההדפסה כך שהשדה יופיע גם על נייר ולא רק על המסך, את שם השדה /T, את סוג השדה /FT, את הדגלים /Ff, ואת הפניית החזרה /P לאובייקט העמוד. אחר כך הוא מוסיף את השדה החדש למערך /Fields של AcroForm ולמערך /Annots של אותו עמוד, תוך פתירת הפניות עקיפות לאורך הדרך, כך שהמערכים האמיתיים מתרחבים במקום להשאיר את הווידג'ט בודד

הרישום הכפול הזה חשוב משום שווידג'ט שחי רק באחת משתי הרשימות נשבר בצורה עדינה. שדה שנמצא ב-/Fields אבל חסר ב-/Annots של העמוד מוכר לטופס אך לעולם אינו מצויר; המצב ההפוך מצויר אבל אינו מוכר ללוגיקת הטופס. HotPDF שומר את שניהם מסונכרנים בכל הוספה, וזו בדיוק מסוג עבודות הניהול שאחרת היית נדרש לבצע ביד מול המפרט בלי לטעות אפילו בפרט קטן

כמה מגבלות שחשוב להכיר

הגדר ציפיות לפני שאתה בונה על זה תהליך עבודה. ההתנהגות של שיטוח והפקה מחדש תלויה בכך שהצופה מכבד את /NeedAppearances, וזה כולל את Acrobat, מנועי ה-PDF של דפדפנים מודרניים ואת קוראי השולחן הנפוצים, אבל זו אינה הבטחה קשיחה לכל מנוע רינדור שקיים בשטח. אם אתה חייב להפיק קובץ שהשדות בו ייראו זהה בכל מקום, כולל בצופים שמתעלמים מהדגל, אתה נמצא בתחום של appearance streams והמסלול של יצירה מאפס שמצייר עבורך /AP הוא ההתאמה הטובה יותר. גם שדה החתימה נוצר כווידג'ט חתימה ריק שמוכן לחתימה; הצבת השדה אינה זהה להחלת חתימה קריפטוגרפית

עבור שינוי של מה שכבר קיים במקום הוספה אליו, הפעולה הקרובה היא שיטוח טפסים, שבה אתה אופֶה את השדות האינטראקטיביים חזרה לתוכן העמוד הסטטי כך שהערכים הופכים קבועים ואינם ניתנים לעריכה; המעבר הזה, כולל האופן שבו מטפלים בטפסי XFA, נידון ב-שיטוח שדות XFA ו-AcroForm ב-Delphi. הוספת שדות ושיטוח שדות הם שני קצות של אותו מחזור חיים: המאמר הזה מראה איך להוסיף אינטראקטיביות למסמך שלא הייתה בו, ושיטוח הוא הדרך להוריד אותה שוב אחרי שהטופס סיים את תפקידו

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