PDFlibPas, ספריית losLab PDF Developer Library לדלפי, כותבת כל מספר שהיא מכניסה לזרם תוכן עם מפריד עשרוני נקודה ובלי exponent, בלי קשר למה שהגדרות האזור של Windows אומרות. מאז v3.539.26 ה-AddPageMatrix, ה-ScalePage, ה-DeskewPage, ה-RedactRegion, פלט ה-text-to-path והצביעה מחדש מעצבים אופרנדים דרך PLDoubleToStrConst, ומאז v3.539.33 הפרסרים שקוראים את המספרים האלה בחזרה משתמשים ב-PLTryStrToFloatInvariant במקום ב-locale של המערכת. על מכונה גרמנית, צרפתית או ברזילאית אותו קוד מניב עכשיו את אותם בייטים כמו על אמריקאית, וזו ההתנהגות היחידה שפורמט קבצים יכול לסבול
למה locale של עשרון פסיק מקלקל PDF בלי שום שגיאה?
locale של עשרון פסיק מקלקל PDF בשקט כי הפסיק אינו תו מספר בתחביר PDF, ולכן הנזק נקרא כ-token-ים תקפים עם משמעות שגויה. לפני התיקון, ה-PLFloatToStr לא היה יותר מקריאה חשופה ל-FloatToStr, וה-FloatToStr הולך אחר FormatSettings.DecimalSeparator. עם מפריד פסיק, AddPageMatrix(0.5, 0.5, 0, 0) כתב 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 מרשה ספרות, נקודה אחת וסימן פותח במספר וכלום מעבר, כך שפרסר תוכן קורא את השורה הזאת כמספר 0 ואחריו token לא מוכר ,5, ואופרטור ה-cm מגיע לבסוף עם אופרנדים שגויים. שום דבר לא מרים חריגה, שום דבר לא נרשם. העמוד פשוט רונדר עם מטריצת טרנספורמציה שסטתה, ולעבוד אחורה מציור במקום הלא נכון אל הגדרת locale הוא אחר צהריים מדכא
הפגם השני מסתתר מאחורי הראשון. ה-FloatToStr משתמש בפורמט ffGeneral, שעובר לכתיב exponent ברגע שהסדר גודל יורד מתחת ל-1E-4, ולכן offset זעיר יצא כ-1E-5. אותו §7.3.3 קובע ש-PDF לא תומך בצורת ה-exponent, מה שאומר שאפילו מכונה עם locale אמריקאי יכלה לכתוב אופרנד לא תקף נתון ערך קטן מספיק. בדיקות הרגרסיה של המהדורה הזאת נועצות בשתי צורות הכישלון: הן הופכות את המפריד לפסיק, קוראות ל-API וסורקות את התוכן המתקבל אחר כל token שמכיל פסיק או exponent
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // מדמה שולחן עבודה de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 ומעלה כותבים: 0.5 0 0 0.25 0.00001 12.75 cm
// build ישנים כתבו: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
שני סוגי מספרים, שתי משפחות של helpers
התיקון ב-PDFlibPas הוא פיצול קפדני: מספרים שמוצגים לאנשים רשאים ללכת אחר ה-locale, ומספרים שנכתבים עבור מכונה לעולם לא. ה-PLFloatToStr וה-PLStrToFloat נשארים ב-PDFlibExtra.pas עבור טקסט מכוון-משתמש, וההצהרה שלהם נושאת עכשיו הערה שאומרת בדיוק זאת. כל דבר שמסתיים כתחביר PDF עובר דרך PLDoubleToStrConst עם מספר קבוע של ספרות עשרוניות שנבחר למשימה: שש עבור מטריצות, ארבע עבור קואורדינטות והתאמות TJ, שלוש עבור צבעים ומלבני FDF. הביקורת עבור v3.539.26 נגעה ביותר אתרי קריאה ממה שדוח הבאג המקורי רמז:
AddPageMatrix,ScalePageו-DeskewPage, שכולם מקדימיםcmלתוכן העמוד הקיים- בוני אלמנטי העמוד שפולטים איפוסי
Tm, התקדמויותTJוטרנספורמציותcm - מטריצות מיקום glyph ונקודות מתאר בממיר ה-text-to-path
- תיבת המילוי השחורה שה-
RedactRegionמקדים, ערכי ה-/Rectבייצוא FDF והאופרנדים שהצביעה מחדש כותבת
ה-PLDoubleToStrConst הוא מעצב כתוב-ידנית ולא wrapper סביב FloatToStrF, ושלוש מהתכונות שלו חשובות כאן. הוא תמיד כותב נקודה וחותך אפסים עוקבים, כך ש-0.5 נשאר 0.5 ולא 0.500000. הוא לעולם לא כותב exponent עבור קלט סופי. וערך שאינו אפס וקטן מהדיוק המבוקש שומר על הספרות המשמעותיות שלו במקום להתמוטט לאפס, כך ש-PLDoubleToStrConst(1E-9, 6) מחזיר 0.000000001; רק ערכים מתחת לכ-5E-16 הופכים ל-0. הכלל האחרון קיים כי עיגול גורם קנה-מידה זעיר לאפס הופך מטריצה תקפה לסינגולרית, וזה באג גרוע יותר מזה שמתקנים
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// פלט מכונה: עשרון נקודה, בלי exponent, אפסים עוקבים נחתכים
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // שומר 4 ספרות משמעותיות
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// קלט מכונה: כישלון רך במקום EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // מספרי תוכן לעולם לא משתמשים בפסיק
end;
למה צד הפרסור מסוכן יותר מצד הכתיבה?
צד הפרסור מסוכן יותר כי פרסר כבול-locale לא מניב מספר שגוי, הוא זורק חריגה. ה-PLStrToFloat קורא ל-StrToFloat, שמעלה EConvertError כשהטקסט לא תואם את המפריד של המערכת. על מערכת עם עשרון פסיק זה אמר שה-RecolorPage נטש ברגע שפגש אופרטור 0.5 g רגיל, כך שכל עמוד מהעולם האמיתי נכשל, לא רק אקזוטיים. ה-RenderPageRegionToFile דחה את פורמט ה-clip המתועד של עצמו, "10.5,20.5,50.5,40.5", ותכונות אורך SVG, צבעי ייצוא SVG, רשימות קודקודים של הערות וערכי solidity של output intent נדחו או הוחלפו בשקט בברירות המחדל. ספרייה שעובדת מושלם על מכונת המפתח ונכשלת אצל הלקוחה הראשונה במינכן היא בדיוק סוג הקוד שנראה נכון רק בזכות המקום שבו נבדק, כמו המקרים במאמר על קוד דלפי שעובד במקרה
v3.539.33 סיווג כל קריאת StrToFloat ו-TryStrToFloat לפי מקור הקלט שלה. אופרנדים של זרם תוכן, תכונות SVG, מחרוזות צבע של הצייר ורשימות clip וקודקודים מופרדות-פסיק כולן בעלות תחביר נקודה קבוע, ולכן הן עוברות עכשיו דרך PLTryStrToFloatInvariant, שחותך רווחים מהטקסט, מפרסר עם PLInvariantFormatSettings ומחזיר False עבור קלט ריק, משובש או אינסופי במקום להעלות חריגה. רשימה מופרדת-פסיק לא משאירה מקום לפשרה, כי פסיק לא יכול להיות גם מפריד הרשימה וגם סימן העשרון. אותו מעבר גם תיקן כתיבה מחוץ לגבולות: ה-RenderPageRegionToFile היה מאחסן ערך clip חמישי מעבר לבאפר בן ארבעת האיברים שלו. עבור pipeline הצביעה מחדש המתואר במדריך המרת PDF למרחב צבע אחד, התוצאה המעשית היא שה-RecolorPage וה-RecolorDocument לא נוטשים יותר על מערכת עם עשרון פסיק. ערכי כללים שקורא מקליד אל CheckDocumentPolicy הם מקרה הפרסור היחיד שמשתמש במקום זאת ב-helper הסלחן, מהסיבה שהסעיף הבא מסביר
מה קורה כשמתקנים רק קצה אחד של round trip?
תיקון קצה אחד בלבד של round trip של locale שובר קוד שפעם עבד, ולכן שינוי תכונות המבנה ב-v3.539.32 הזיז את הכותב ואת הקורא יחד. ה-wrapper-ים של ה-SetStructElem* מעבירים מספרים כמחרוזות: ה-SetStructElemBBox מעצב ארבעה ערכים למחרוזת אחת, מאחסן אותה דרך AddTagAttribute, והכותב של ה-/A מפרסר אחר כך את המחרוזת כדי להחליט אם היא הופכת למספר, למערך או לשם. שני הקצוות השתמשו ב-locale של המערכת, ולכן על מערכת עם עשרון פסיק ה-round trip היה עקבי-עצמו. הבאג התגלה רק כשקורא הלך אחר התיעוד והעביר "0.5" אל ה-AddTagAttribute: הקורא לא הצליח לפרסר אותו ופלט את שם ה-PDF /0.5. ל-placeholder של ה-PDF/VCR הייתה הבעיה בתמונת מראה, כי הספרייה ייצרה GTS_BBox עם נקודה ואז אימתה אותו עם ה-locale לפני השמירה
שינוי הכותב בלבד לנקודה היה גרוע מלא לעשות כלום, כי כל ערך SetStructElem* היה אז נכשל אצל הקורא הכבול-locale ומידרדר לשם. לכן הכותבים משתמשים עכשיו ב-PLDoubleToStrConst(v, 6), והקורא משתמש ב-PLTryStrToFloatLenient החדש, שמנסה קודם את הצורה עם הנקודה ונופל חזרה אל ה-locale של המערכת. קורא עם locale של פסיק שהעביר "1,25" בעבר עדיין מקבל את המספר 1.25. הפשרה מכוונת ומתועדת: על מערכת גרמנית "1.500" היה הופך לשם כי ה-StrToFloat דוחה מפרידי אלפים, והוא נקרא עכשיו כ-1.5, בזמן שמחרוזות מילוליות NAN ו-INF כבר לא מתקבלות כמספרים
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // קורא עם עשרון פסיק
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, היה /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // עדיין /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
איפה NaN ואינסוף נעצרים
ה-AddPageMatrix, ה-ScalePage וה-RedactRegion דוחים עכשיו ארגומנטים של NaN ואינסוף כבר מלפנים ומחזירים 0, כי אף מספר PDF לא יכול לייצג אותם. ה-ScalePage כבר סירב לגורמים של אפס ומטה, אבל NaN עובר בדיקת <= 0, ולכן scale של NaN היה נוסע כל הדרך אל המעצב. ב-v3.539.26 אותו מעצב עדיין קרא ל-Round על NaN, מה שמעלה EInvalidOp על Win32 שבו יחידת ה-x87 לא מסווה פעולות לא תקפות; v3.539.31 גרם ל-PLDoubleToStrConst לכתוב 0 עבור NaN כקו הגנה אחרון, אבל אפס בתוך מטריצה הוא טרנספורמציה סינגולרית, ולכן הבדיקה ברמת ה-API נשארת התיקון האמיתי. שני גבולות נשארים במקומם בכוונה. מחרוזות מצב מטאפייל נכתבות ונקראות עם ה-locale בתוך תהליך אחד ולעולם לא עוזבות אותו, ולכן השאירו אותן לבד. ובדיקה שמעצבת 1E-5 דרך נתיב אלמנטי העמוד חייבת לקרוא את התוכן לפני שהשכבה נכתבת מחדש, כי פליטת אופרנדים מחדש בדיוק המסמך הופכת כחוק את הערך הזה ל-0
אם היישום שלכם מגיע אל לקוחות מחוץ לעולם העשרון-נקודה, ההרגל הבטוח ביותר הוא זה שסוויטת הבדיקות של PDFlibPas משתמשת בו עכשיו: הריצו את הנתיבים המייצרים PDF פעם אחת עם FormatSettings.DecimalSeparator מוגדר לפסיק וסרקו את הפלט אחר פסיקים ו-exponent-ים. המאמר על שימור דיוק עשרוני מפורסר מכסה את החצי השני של אותו סיפור, איך מספרים שנקראו מקובץ קיים שומרים על הטקסט המדויק שלהם בשמירה. הורדות, הפניית ה-API המלאה וה-build הניסיוני נמצאים בעמוד המוצר של PDFlibPas Delphi PDF library