מאמר טכני

שדות PDF רבי-בחירה ב-round trips של FDF ו-XFDF (דלפי)

HotPDF מעביר ערכי list box רבי-בחירה סבב שלם דרך FDF ו-XFDF על ידי שמירה על ערך השדה כמערך מקצה לקצה. מאז גרסה 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF ו-ExportLoadedFormToXFDF כותבים כל אפשרות נבחרת כמחרוזת FDF משלה או כאלמנט <value> משלה של XFDF, ומתודות הייבוא המתאימות בודקות כל ערך מול אפשרויות השדה ובונות מחדש את אינדקסי הבחירה של ה-/I לפני שהן משנות משהו. שום דבר לא מודבק למחרוזת אחת בדרך

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

למה חיבור ערכי רב-בחירה עם מעברי שורה שובר את ה-round trip?

חיבור הבחירות למחרוזת אחת זורק את הגבולות בין הערכים, וערך יכול להכיל את המפריד, ולכן אף מייבא לא יכול לפצל את המחרוזת חזרה נכון. ISO 32000-1 §12.7.4.4 מתיר לרשומת ה-/V של שדה בחירה להיות או מחרוזת טקסט יחידה או מערך של מחרוזות טקסט, ו-list box עם דגל MultiSelect (ביט 22 של /Ff) משתמש בצורת המערך ברגע שנבחרה יותר מאפשרות אחת. אותו סעיף מגדיר את ה-/I כמערך של אינדקסי אפשרויות מבוססי-0 בסדר עולה, שמציגים משתמשים בו כדי להבחין בין שתי אפשרויות שקרה להן לחלוק ערך export. ב-HotPDF ה-getter הסקלרי GetFormFieldValue קורא רק את צורת המחרוזת, ולכן הרצת מערך דרכו שדרגה את הייצוא למחרוזת ריקה, והייבוא הישן של XFDF חיבר אלמנטי <value> חוזרים עם LF. תארו לכם אפשרות שיוצאה כ-Deep, הזנת שורה, Blue: אחרי החיבור, Deep\nBlue\nRed יכולה להיות שתי בחירות או שלוש, והקובץ לא נותן דרך לדעת איזה. התיקון היה להפסיק להשתמש בסקלר באמצע ה-round trip לגמרי

round trip רב-בחירה ישן של HotPDF שבו שתי אפשרויות list box שנבחרו, אחת מכילה מעבר שורה מוטמע, מושטחות על ידי הנתיב הסקלרי של GetFormFieldValue אל המחרוזת היחידה Deep, הזנת שורה, Blue, הזנת שורה, Red, שקוראים במורד הזרם יכולים לפענח הן כשתי בחירות והן כשלוש
חיבור ערכי רב-בחירה למחרוזת אחת הורס את גבולות הערכים, וערך ייצוא שמכיל בעצמו מעבר שורה הופך את הצורה המשוטחת לעמומה

מה קבצי ה-FDF וה-XFDF המיוצאים מכילים?

HotPDF כותב ערך רב-בחירה כמערך מוטיפוס ב-FDF וכאלמנט <value> אחד לכל בחירה ב-XFDF, כך שהגבולות נשארים גלויים על הדיסק. ב-FDF כל פריט שומר על הכתיב שהיה לו ב-PDF המקור: מחרוזות הקסדצימליות יוצאות כ-hex, ומחרוזות literal מוברחות על ידי helper יחיד שהופך CR ו-LF אל \r ו-\n. ב-XFDF השורש נושא xml:space="preserve" כפי ש-ISO 19444-1 דורש, מה שאומר שכל רווח בתוך אלמנט טקסט נספר כנתון. HotPDF לכן כותב את תג הפתיחה, את הטקסט המוברח ואת תג הסיום של כל <value> בחתיכה אחת, משאיר הזחה מחוץ לאלמנט, ומקודד CR, LF ו-TAB כהפניות תווים כך ש-parser של XML שמחיל נרמול סיומי-שורה לא יוכל לשנות את הבתים המקוריים

צורות הייצוא ש-HotPDF כותב עבור list box רב-בחירה מאז 2.755.0: FDF נושא מערך מוטיפוס אחד לכל שדה עם /V [(Deep line break Blue) (Red)] וערך הקסדצימלי של אזור, בזמן ש-XFDF נושא אלמנט value אחד לכל בחירה תחת xml:space preserve כך שרווח נספר כנתון
הגבולות נשארים גלויים על הדיסק: FDF שומר כל בחירה כפריט מערך משלה ו-XFDF כותב כל אחת באלמנט value נפרד, כך שאף מייבא לא צריך לנחש
<!-- FDF: מערך מוטיפוס אחד לכל שדה -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: <value> אחד לכל בחירה -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

שני מקרי קצה של ייצוא שווים להכיר לפני שכותבים את קוד הקריאה. ראשית, ExportLoadedFormToFDF בונה את גוף ה-FDF המלא בזיכרון לפני שהוא יוצר את קובץ היעד (תוקן ב-2.755.1), כך שערך שלא ניתן לייצוא, כמו מערך שמחזיק משהו שאינו מחרוזות, מעלה חריגה בלי לקצץ קובץ קיים. שנית, בחירה ריקה ב-list box שמציע גם ערך export של מחרוזת ריקה עמומה ב-XFDF, כי <value/> יכול לפרוש שלא נבחר כלום או שהאפשרות הריקה נבחרה. ExportLoadedFormToXFDF מעלה חריגה במקרה כזה במקום לנחש, והוא מעלה לפני שקובץ היעד נפתח. ל-FDF אין עמימות כזאת, כי /V [] ו-/V [()] נבדלים. שני מייצאי ה-FDF גם מדלגים על קצוות widget בלבד שאין להם שם /T, בהתאמה למייצא ה-XFDF, כי אף מייבא לעולם לא יכול להתאים את הרשומות האלה בחזרה אל שדה

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // list box רבי-בחירה נכתבים כ-/V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // בחירה ריקה בתוספת אפשרות export ריקה: XFDF לא יכול להבחין
          // ביניהם, והקובץ ה-.xfdf הקיים נשאר ללא מגע
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

איך HotPDF מאמת ערך רב-בחירה בייבוא?

HotPDF מקבל מערך מיובא רק כשהיעד הוא שדה בחירה עם דגל MultiSelect מוגדר וכל ערך במערך תואם ערך export במערך ה-/Opt של השדה. כל משבצת אפשרות יכולה להיכבש פעם אחת, ולכן רשימה עם שתי אפשרויות שחולקות את ערך ה-export b מקבלת [<62> <62>] כשתי בחירות נבדלות ודוחה b שלישי. ה-/I הנבנה מחדש עוקב אחר סדר ה-/Opt ולא אחר סדר הערכים הנכנסים, כפי ש-§12.7.4.4 דורש אינדקסים עולים. HotPDF בונה את ה-/V וה-/I החדשים כאובייקטים מנותקים ומקצה אותם רק אחרי שכל ערך עבר אימות, כך שערך נדחה לעולם לא משאיר מאחור חצי מערך או אינדקסים מיושנים. העותק נכתב אל השדה שמיובא ולא אל מערך אב משותף, כתיבי hex שמגיעים מ-FDF נשארים hex דרך השמירה, ושדות שהחישובים שלהם תלויים ב-list box מסומנים לחישוב מחדש. אם צריך רק להגדיר ערך יחיד, קביעת ערך שדה טופס אחד ב-PDF טעון עוברת דרך הנתיב הסקלרי, שעל פי עיצובו אינו מטפל בבחירות מרובות

אימות ייבוא של HotPDF עבור ערכי רב-בחירה: היעד חייב להיות שדה בחירה עם MultiSelect מוגדר ב-/Ff, כל ערך נכנס חייב להתאים ערך export של /Opt כשכל משבצת נכבשת פעם אחת, ה-/I נבנה מחדש עולה בסדר ה-/Opt, וה-/V וה-/I המנותקים מוקצים רק אחרי שכל הערכים עוברים
כל ערך נכנס נבדק מול אפשרויות השדה לפני שכותבים משהו, ולכן ערך נדחה לעולם לא משאיר מאחור חצי מערך או אינדקסי בחירה מיושנים

כמה כלים אחרים כותבים ערכי export של ASCII פשוט כמחרוזות hex בלי סימן סדר בתים, למשל <416272>, ואז מייצאים XFDF על ידי כתיבת הספרות ההקסדצימליות האלה כטקסט. השוואת literal קפדנית בדרך חזרה נכשלת, והייבוא נפסק. גרסה 2.755.1 מוסיפה ניסיון חוזר אחד: כשערך לא תואם אף אפשרות, HPDFHexSpellingText מפענח את הטקסט כמטען hex ומשווה את התוצאה שוב. הניסיון החוזר חל רק על קלט שאחרת היה מעלה חריגה, ולכן הוא לעולם לא משנה ערך שכבר נתאם. אותה מהדורה גם הפכה את הנתיב הסקלרי והמערכי להשתמש באותו מפענח Unicode, שמבין PDFDocEncoding, UTF-16 עם כל סימן סדר בתים ו-UTF-8. לפני זה, ערך לוגי אחד יכול היה להתאים בנתיב אחד ולהיכשל באחר במסמכים שערבבו קידודים

למה קובץ FDF תקין עדיין יכול לאבד שדות בפענוח?

סורק FDF שאינו עוקב אחר מחרוזות הקסדצימליות יכול לחתוך מילון שדה לשניים כשערך hex נגמר בדיוק ליד מסיים המילון. ב-<< /T (region) /V <416273>> ה-> הראשון סוגר את המחרוזת ההקסדצימלית, אבל סורק נאיבי קורא אותו יחד עם ה-> הבא כסוף המילון ושומט בשקט את השדה. מייבא ה-FDF ברמת הקובץ כבר עקב אחרי השאלה האם הוא בתוך מחרוזת הקסדצימלית, וב-2.755.1 הסורקים של המערך והמילון מאחורי ImportLoadedInterchangeFromFDF עושים זאת גם כן. נושא שני נוגע להפניות עקיפות. קובץ FDF הוא מסמך תחביר-PDF קטן עם מספור אובייקטים משלו (ISO 32000-1 §12.7.7), ולכן ערך כמו /V [11 0 R] מתייחס לאובייקט 11 של קובץ ה-FDF, לא לאובייקט 11 של ה-PDF שאתם ממלאים. ה-parser המפושט של FDF ב-HotPDF לא פותר הפניות בתוך הקובץ, ולכן הוא דוחה מערך כזה במקום לקרוא את אובייקט 11 שקרה להיות במסמך היעד

ייבוא קובץ, זרם ו-XFDF מדווחים על שגיאות אחרת

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

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // שדות בלבד; ערך מחוץ ל-/Opt או יעד שאינו רב-בחירה מעלה חריגה
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

הרחבת ה-callbacks של XFDF בלי לשבור קוראים קיימים

התמיכה במערכים ביחידת ה-XFDF הנמוכה יותר חיה ברשומה נפרדת, THPDFXFDFArrayAccess, וב-overloads חדשים של HPDFXFDFExportFields ו-HPDFXFDFImportFields, לא בשדות נוספים שהתווספו לסוף הרשומה הקיימת THPDFXFDFAccess. הסיבה היא תאימות בינארית. קוד שממלא THPDFXFDFAccess כמשתנה מקומי לעיתים קרובות מגדיר רק את המשבצות שהוא מכיר ולעולם לא מנקה את השאר, ולכן מצביע פונקציה חדש שנוסף לרשומה הזאת יכיל זבל מחמסנית, והספרייה הייתה לוקחת אותו כ-callback אמיתי. עם רשומה נפרדת, קוראים ישנים שומרים על הפריסה הישנה ועל ה-overloads הישנים, וה-overloads האלה מעבירים רשומת מערך all-nil בפנים. overload הייבוא הסקלרי המקורי עדיין מחבר ערכים חוזרים עם LF עבור תאימות, ורק ה-overload מודע-המערך מפריד ביניהם. כשקושרים חנות נתונים משלכם, מתחילים מ-Default(THPDFXFDFArrayAccess). מחזירים True מה-GetFormFieldValueArray עבור כל שדה בעל ערך-רשימה, כולל כזה שבו לא נבחר כלום, ו-False כדי לחזור אל ה-callback הסקלרי

uses HPDFXFDF;

// מצביע פונקציה פשוט, לא "of object": ה-Context נושא את החנות שלכם
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // הקשרות הסקלריות הקיימות שלכם
  ArrayAccess := Default(THPDFXFDFArrayAccess); // כל משבצת לא בשימוש היא nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

חילוף רב-בחירה עובד על list box-ים שכבר קיימים ויש להם את ביט ה-MultiSelect מוגדר ב-/Ff. לגבי איך שדות בחירה וביטי הדגל שלהם נוצרים מלכתחילה, ראו הוספת ListBox ושדות AcroForm אחרים ל-PDF טעון. לגבי סימון הערות שעובר דרך עץ ה-<annots> של ה-XFDF, ראו ייבוא וייצוא הערות XFDF ב-HotPDF. ההפניה המלאה ל-API וההורדה לניסיון נמצאים בעמוד רכיב HotPDF ל-PDF בדלפי