PDF Library for Delphi (PDFlibPas) פולט JSON תקף עבור כל מספר PDF מאז v3.539.31. ה-GetObjectJSON כותב מחדש token-ים ש-ISO 32000-1 מקבל אבל RFC 8259 דוחה, כמו -.25, +1.5 ו-007.5, ל--0.25, 1.5 ו-7.5 ספרה-אחר-ספרה; ה-GetDocumentJSON ודוחות הניתוח כותבים null עבור NaN ו-Infinity; וה-PLDoubleToStr כותב 0 עבור NaN במקום להעלות EInvalidOp באמצע ייצוא. לפני התיקון, הספרייה יכלה להניב JSON שהקורא שלה עצמה סירב לטעון בחזרה
למה מספר PDF תקף שובר JSON?
כי שני הדקדוקים לא מסכימים על ארבעה פרטים קטנים, ופרסר PDF שמכבד את טקסט המקור יעביר את הפרטים האלה ישר אל הפלט. ISO 32000-1 §7.3.3 מרשה למספר להתחיל בסימן פלוס, לוותר על החלק השלם (.5), להיגמר בנקודה עירומה (4.) ולשאת אפסים מובילים (007.5). RFC 8259 §6 לא מרשה אף אחד מאלה: מינוס אופציונלי, חלק שלם שהוא או 0 או מתחיל ב-1 עד 9, ולפחות ספרה אחת אחרי כל נקודה עשרונית. מפיקים חופשיים לכתוב את הצורות של PDF, והרבה מחוללים וקבצים שנערכו ביד אכן עושים זאת
הדליפה באה מתכונת דיוק מכוונת. מאז v3.539.19, ה-TPDFNumeric.Output מחזיר את הטקסט המדויק שה-tokenizer פרסר עבור מספרים ממשיים, וזה מה שמשאיר ערך צבע מכויל מדויק בשמירה, כפי שמתואר בשימור דיוק עשרוני מפורסר של PDF. ה-tokenizer כבר מטפל ב-.5 לתוך 0.5 וב-4. לתוך 4.0 בדרך פנימה, ומספרים שלמים מעוצבים מחדש מהערך שלהם, כך ש-+3 חוזר כ-3. מה ששורד מילה-במילה הוא השאר: נקודה פותחת עם סימן (-.25), פלוס מפורש על ממשי (+1.5) ואפסים מובילים (007.5). הכותב הישן של האובייקטים חיבר את ה-Output מיד אחרי "value":, וה-TJSONParser.ParseNumber בקורא של הספרייה עצמה עוצר על כל אחד מאלה עם "Invalid JSON number", כך שהייצוא הצליח והייבוא החוזר נכשל עם PDFLIB_ERROR_OBJECT_JSON_INVALID (105)
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
JSON: AnsiString;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('legacy-drawing.pdf', '') = 0 then
raise Exception.Create('load failed');
// אובייקט 12 הוא מערך שנכתב כ-[-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 ומעלה: הערכים מגיעים כ: -0.25, 1.5, 7.5
// ל-SetObjectJSON אין אפשרויות, ולכן מעבירים 0
if Lib.SetObjectJSON(12, JSON, 0) = 0 then
raise Exception.CreateFmt('round trip rejected, error %d',
[Lib.LastErrorCode]);
Lib.SaveToFile('legacy-drawing-roundtrip.pdf');
finally
Lib.Free;
end;
end;
איך PDFNumberTextToJSON שומר על כל ספרה?
ה-PDFNumberTextToJSON מאיית מחדש את ה-token במקום לחשב אותו מחדש מתוך Double. הפונקציה ב-PDFlibObjectJSON קוראת סימן אופציונלי, אוספת ספרות לפני ואחרי נקודה עשרונית אחת, ואז מיישמת רק את העריכות ש-JSON דורש: היא משמיטה פלוס, משייפת אפסים מובילים בזמן שהיא שומרת אחד, מספקת 0 כשהחלק השלם ריק, משמיטה נקודה עירומה בסוף ומחזירה את המינוס. token שמכיל תו אחר כלשהו, או בכלל לא מכיל ספרות, נופל חזרה אל PLJSONNumber(Value, 10), שכותבת null כשהערך אינו סופי
- ה-
-.25הופך ל--0.25, וה-+.5הופך ל-0.5 - ה-
+1.5הופך ל-1.5 - ה-
007.5הופך ל-7.5, בזמן שה-0.75נשאר כמו שהוא - ה-
4.הופך ל-4אם token כזה מגיע פעם אל הכותב - ה-
2.22221וה-1.250000שומרים על כל ספרה שברונית, אפסים עוקבים כלולים
עיצוב מה-Double המאוחסן היה קצר יותר ושגוי, מאותה סיבה שבגינה קיים תיקון הדיוק: דיוק הפלט המוגדר כברירת מחדל הוא ארבע ספרות עשרוניות, ואפילו המרה בדיוק מלא יכולה להוסיף רעש בינארי ל-literal עשרוני. שמירת הספרות אומרת שה-SetObjectJSON וה-ImportObjectJSON, שמוסרים כל טקסט מספר JSON אל ה-tokenizer של ה-PDF, משחזרים בדיוק את אותו ערך. הערובה מכסה את הערך, לא את הבייטים: אחרי ייבוא חוזר, ה--.25 מאוחסן ונשמר כ--0.25. שתי האיותים שווים תחת §7.3.3, אבל diff ברמת בייטים יסמן את השינוי, ולכן אל תתייחסו אל מחזור ייצוא וייבוא כאל no-op על מסמך שבייטים שלו מכוסים בחתימה
מה קורה למספר ש-JSON לא יכול לייצג?
ה-GetDocumentJSON כותב עכשיו null עבור כל מספר שהוא NaN או אינסופי, כי ל-RFC 8259 §6 אין תחביר עבור אף אחד מהם. אינסוף קל יותר לייצור ממה שזה נשמע: ה-tokenizer של ה-PDF צובר ספרות בכפילה חוזרת אל תוך Double, שמגיע לקצה סביב 1.8 × 10308, כך ש-literal של מספר שלם באורך חודש מעל 300 ספרות הופך בשקט ל-+Inf. קבצים כנים לעולם לא מכילים literal כזה; קבצים מ-fuzz ועוינים כן, ולכן הם שייכים לאותו קורפוס בדיקות כמו המקרים בחיזוק פרסר PDF בפסקל נגד קבצים זדוניים. הכותב הישן של המסמכים עיצב לא-שלמים עם Str(D:0:6), ועבור +Inf זה כותב את הטקסט +Inf, שאף צרכן JSON לא יפרסר
ה-null הוא מאבד-מידע בכוונה. צרכנים של פלט ה-GetDocumentJSON חייבים לקבל null בכל מקום שבו מספר יכול להופיע, וראוי שיקראו אותו כ-"היה ערך אבל אין אפשרות לייצג אותו", לא כמפתח חסר. את ה-literal המקורי אי אפשר לשחזר מה-document JSON, ולכן pipeline שאכפת לו ירשום את האובייקט ל-log ויתייחס אל הקובץ כחשוד במקום להחליף בברירת מחדל
למה NaN אחד יכול היה לקטוע ייצוא SVG או JSON?
כי ה-PLDoubleToStr, מעצב המספרים האינווריאנטי שעומד מאחורי זרמי תוכן, SVG, XML, CSV ורוב ה-JSON בספרייה, הכפיל את הקלט שלו וקרא ל-Round, וה-Round(NaN) מעלה EInvalidOp על יעדים כמו Win32, שבהם דלפי משאירה את חריגת הפעולה-הלא-תקפה של ה-x87 לא ממוסכת. החריגה נורתה אחרי שהכותב כבר פלט חלק מהפלט שלו, ולכן מדידה מנוונת אחת, 0/0 במדד או NaN שקורא העביר, השאירה מאחור קובץ קטום. ה-PLDoubleToStr מחזיר עכשיו 0 עבור NaN, והענף השלם שלו נצמד ל-±9.2e18 כמו הענף השברוני, כך שגם Infinity יוצא כ-literal סופי
אפס הוא התשובה הנכונה עבור זרם תוכן, שבו slot של מספר חייב להחזיק מספר, והתשובה הלא נכונה עבור דוח, שבו 0 היא מדידה סבירה. כותבי JSON שחייבים לשמור על ההבחנה משתמשים ב-PLJSONNumber(Value, Decimals) מתוך PDFlibExtra, שכותבת null עבור NaN או Infinity וספרות אינווריאנטיות אחרת. ה-PLJSONNumber תומך עכשיו ב-GetSimilarImageDeduplicationReportJSON, ב-GetAnnotationHitsJSON ובדוחות ה-barcode, ה-deskew, הטקסט המובנה וה-PDF/VCR; דוח ה-deskew כתב בעבר 0 עבור זווית לא סופית וכותב עכשיו null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// מעצבים כל Double לטקסט קודם; PLJSONNumber כותב null
// עבור NaN או Infinity ותמיד משתמש בנקודה עשרונית
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// לעולם לא B.Append(Angle): ה-overload של Double הולך אחר ה-locale של המשתמש
Result := B.ToString;
finally
B.Free;
end;
end;
איפה ה-locale של המשתמש עדיין מתגנב ל-JSON?
דרך כל מעצב שמתייעץ בהגדרות אזור, וביקורת מלאה של הפלט הקריא-למכונה מצאה בדיוק אחד שנותר: ה-maxAcceptedMeanError ב-GetSimilarImageDeduplicationReportJSON, שנכתב עם PLFloatToStr, wrapper דק על ה-FloatToStr. על שולחן עבודה שמפריד העשרונות שלו פסיק, הדוח הכיל "maxAcceptedMeanError":1,5, שפרסר JSON קורא כערך 1 ואחריו token תועה. השדה מדווח על שגיאת הפיקסל הגרועה שהתקבלה מדה-דופליקציה תפיסתית של תמונות, והוא עובר עכשיו דרך PLJSONNumber(Stats.MaxAcceptedMeanError, 6). מלכודת שנותרה היא ה-PLStringBuilder: בדלפי הוא alias פשוט של System.SysUtils.TStringBuilder, שה-Append(Double) overload שלו מעצב דרך ה-locale של המשתמש, בזמן ש-build-ים של FPC משתמשים במחלקת ספרייה במקום, ולכן בדיקה על Free Pascal או על מכונת en-US לעולם לא תתפוס את זה
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// משחזר שולחן עבודה גרמני או צרפתי בתוך ריצת הבדיקה
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// משתמשים ב-fixture שבאמת מכיל תמונות כמעט-שכפוליות,
// אחרת שגיאת הממוצע היא 0 והבאג נשאר חבוי
Lib.LoadFromFile('scanned-batch.pdf', '');
// ריצת יובש עם ספי 2, 2, 4: המסמך אינו משתנה
Lib.GetSimilarImageDeduplicationReportJSON(2, 2, 4, Report);
Parsed := TJSONObject.ParseJSONValue(Report);
if Parsed = nil then
raise Exception.Create('report is not valid JSON on a comma locale');
Parsed.Free;
finally
Lib.Free;
end;
end;
סוויטת רגרסיה עבור פלט JSON צריכה שלושה fixture-ים כדי להישאר ישרה: עמוד שנושא -.25, +1.5 ו-007.5, אובייקט שמחזיק מספר שלם בן 400 ספרות, וכל דוח שרץ תחת locale של פסיק — כל אחד מאומת עם פרסר קפדני ולא במבט עין. object JSON, document JSON ודוחות הניתוח ב-PDF Library for Delphi חולקים את אותם כללי מספרים בין דלפי, C++Builder ו-Free Pascal; רשימת התכונות המלאה נמצאת בעמוד המוצר של PDF Library for Delphi