PDFlibPas (PDF Library for Delphi) בודק כל אובייקט מול טבלה של כללי גרסת PDF לפני שהוא כותב קובץ, ועד לאחרונה ה-preflight של גרסת PDF הזה בלבל מילוני מדידה של CAD רגילים עם גיאו-מרחביים. שרטוט CAD של עמוד אחד נטען מצוין, ואז SaveToFile החזיר 0 עם LastErrorCode 602 ודרש 1.7 ExtensionLevel 3. הכללים המתוקנים מתייחסים למילוני /Measure rectilinear (/Subtype /RL) כאל PDF 1.6 פשוט ושומרים את שער ה-extension לסמנים גיאו-מרחביים אמיתיים
הקובץ הגיע דרך קליטת קורפוס: עמוד אחד, קבוצת optional-content אחת, שני viewport-ים של מדידה rectilinear — הסוג של פלט שחבילת CAD אדריכלית כותבת כדי שמציג יוכל לקרוא מרחקים מתוכנית קומה. שום דבר בו לא היה אקזוטי, ובדיוק בגלל זה הסירוב היה משמעותי. preflight שחוסם קובץ תקין גרוע מאחד איטי, כי הקורא ל-API מקבל אבחנה שנראית סמכותית ומצביעה על תכונה שהמסמך לא מכיל. התיקון לקח שני חלקים: הקריאה במפרט שעומדת מאחורי כלל אחד, וההבנה שהכלל לא יכול היה להבדיל בין שני סוגי המילונים ברמה שבה הוא הביט
איך עובד ה-preflight של גרסה בזמן שמירה ב-PDFlibPas?
שער השמירה, PrepareAndCheckSaveVersion, משווה כל אובייקט עקיף מול PDFFeatureRules ונכשל בכלל הראשון שגם תואם וגם דורש יותר ממה שהיעד מתיר. היעד הוא גרסת המסמך (או הגרסה שנקבעה על ידי LockSaveVersion), בתוספת רמת ה-extension של Adobe שמוצהרת תחת /Extensions /ADBE. כל רשומה של TPDFFeatureRule נושאת MinVersion, MinExtensionLevel, MatchKind כמו fmkDictKey או fmkDictSubtype, מחרוזת Match, שם Feature קריא לאדם ו-callback אופציונלי. AddRule רושם כלל גרסה פשוט; AddExtensionRule תמיד קובע את MinVersion על 17 ומוסיף רמת extension מעל זה, כך שכלל extension יכול להתקיים רק על ידי PDF 1.7 בתוספת רשומת /Extensions נכונה. כשהשער נתקע, גרסת החובה ושם התכונה נשמרים עבור הקורא ל-API, ומפתחות GetInformation 311, 312 ו-313 חושפים אותם
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: גרסת החובה, 312: התכונה שגרמה לה,
// 313: הגרסה שבה יעד השמירה נעול ('' כשאין נעילה)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
למה שרטוט CAD פשוט נכשל עם שגיאה 602?
טבלת הכללים הכילה AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), שירתה על כל מילון שרק החזיק מפתח /Measure, ולכל viewport מדידה יש כזה. מערך ה-/VP של העמוד מחזיק מילוני viewport, כל viewport מצביע על מילון המדידה שלו דרך /Measure, והתאמת נוכחות-המפתח עצרה שם בלי להביט מה מילון המדידה באמת היה. סריקת התכונות בזמן הטעינה יכלה אז להעלות את מספר גרסת המסמך אל 1.7, אבל היא מעולם לא כותבת הצהרת /Extensions בשם קובץ קלט, ולכן שער השמירה ראה PDF 1.7 ברמת extension 0 ודיווח 1.7 ExtensionLevel 3. הסירוב הזה להמציא הצהרת extension הוא מכוון: הספרייה לא מקדמת בשקט קובץ קלט כדי לטשטש כלל שגוי
המפרט חד-משמעי לגבי המקרה ה-rectilinear. מילוני Measure הגיעו ב-PDF 1.6, ו-ISO 32000-1 §12.9 נותן ל-/Subtype ברירת מחדל של RL, מערכת קואורדינטות rectilinear המתוארת על ידי סט הרשומות שלה עצמו: יחס קנה מידה, פורמטי מספרים של X ו-Y, מרחק ושטח. מדידה גיאו-מרחבית היא התוספת המאוחרת מ-Adobe Extension Level 3 על גבי PDF 1.7, מזוהה על ידי /Subtype /GEO ונושאת מערכי נקודות גיאוגרפיים, מילוני מערכות קואורדינטות ויחידות תצוגה — המבנים שהקריאה של viewport-ים של GeoPDF ומערכי GPTS ו-LPTS בדלפי עוברת עליהם. שני המילונים תלויים על אותו מפתח /Measure, ולכן כל כלל שעוצר במפתח לא יכול להיות נכון לשניהם. המידע המבדיל יושב רמה אחת מטה, במילון המדידה עצמו
מה ערכת הכללים המתוקנת עדיין אוכפת?
התיקון מוחק את כלל המפתח הלא-מותנה ומשאיר את השערים שמתארים דרישות גרסה אמיתיות. עמוד שנושא /VP או /UserUnit עדיין זקוק ל-PDF 1.6 דרך CB_PagePDF16Entries, מפתח /PtData עדיין זקוק לרמת extension 3, וה-CB_GeospatialDictionary מחליט אם מילון מדידה הוא גיאו-מרחבי לפי התוכן שלו ולא לפי המפתח שדרכו הגיע אליו
// הוסר: כל מילון עם מפתח /Measure נחשב גיאו-מרחבי
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
הרגרסיות המשותפות של Delphi ו-FPC קובעות את הגבול הזה משני הצדדים. viewport שמילון המדידה שלו משמיט את /Subtype ואחד שמאיית אותו במפורש כ-/RL שניהם עוברים ב-PDF 1.6, אותו עמוד עדיין נדחה ב-PDF 1.5, וזיהוי התכונות כבר לא מדווח extension עבורו. הוספת מערך /GPTS מהפכת את פסק הדין בחזרה אל 1.7 ExtensionLevel 3, שעובר ברגע שרמת ה-extension מוצהרת, ומילון /Subtype /GEO חשוף נדחה בלעדיה. ה-callback שמרני במתכוון: מילון rectilinear שנושא גם מפתח /GCS או /PDU תועה מתייחס כגיאו-מרחבי, כי למפתחות האלה אין משמעות במודל ה-RL
LockSaveVersion הוא המקום שבו השינוי הזה נראה לקוראי ה-API. TPDFlib.LockSaveVersion מקבל '1.0' עד '1.7', מחזיר 0 על כל דבר אחר, קובע את גרסת המסמך ועוצר קריאות מצד הכותב מלהעלות אותה בשקט, ובכל זאת שער השמירה עדיין רץ מול הערך הנעול. עם הכללים המתוקנים, קובץ CAD נעול על 1.6 נשמר נקי. GeoPDF אמיתי נעול על 1.6 עדיין מקבל 602, וזו התשובה הנכונה, וקריאות הכתיבה הגיאו-מרחביות כמו SetMeasureDictCoordinateSystem מצהירות בעצמן רמת extension 3 כשבונים את התוכן הזה דרך ה-API
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// תוכן אמיתי מעל 1.6, למשל מילון מדידה GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
למה סריקת כללי הגרסה הייתה איטית מכפי שצריך?
הסריקה העתיקה כל TPDFFeatureRule לרשומה מקומית לפני בדיקתו, ומכיוון שהרשומה מחזיקה שני שדות AnsiString, כל העתקה כיוונה שני מוני הפניות ושחררה את הערכים הקודמים. ה-preflight מבקר בכל צומת של כל עץ אובייקטים, סקלרים כלולים, כך שהעלות הזאת הכפילה את מספר האובייקטים במספר הכללים, וכללים שלא חלו בכלל על גרסת היעד הועתקו קודם ודולגו אחר כך. מכיוון ש-PDFFeatureRules ממולא פעם אחת באתחול היחידה ומתייחסים אליו כקריא-בלבד, v3.539.17 מעביר רשומות טבלה ישירות אל MatchSingleRule ו-RuleExceedsTarget, שהפרמטרים const Rule שלהן לוקחים הפניה בלי לגעת במחרוזות
// לפני: העתקת רשומה מנוהלת לכל כלל, לכל אובייקט מבוקר
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// אחרי: פרמטרי const קוראים את רשומת הטבלה הבלתי ניתנת לשינוי במקום
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
האפקט הנמדד מצומצם וכך צריך לצטט אותו. הבנצ'מרק בודק מערך של 20,000 אובייקטים מספריים מול יעד PDF 1.4 עשר פעמים בסבב; בנוי עם FPC Win64 ב--O2, החציון של חמישה סבבים צנח מ-0.711 שניות ל-0.203 שניות, והרצת שני ה-builds בסדר הפוך נתנה 0.459 שניות מול 0.150 שניות. זה בערך רווח של פי 3 על נתיב התאמת הכללים בלבד. שמירה אמיתית גם משלמת עבור זיהוי תכונות נדחה, פענוח אובייקטים וסריאליזציה, ולכן היחס לא מועבר אל זמן השמירה הכולל. סדר הכללים, ה-callbacks, ספי הגרסה והאבחנה של הכשל הראשון ללא שינוי, ואף כלל לא הועבר ל-cache בין שמירות או דולג כדי להגיע לשם
מה כדאי לבדוק כש-PDF טעון נכשל ב-preflight הגרסה?
קוראים את המפתחות 311 ו-312 לפני שנוגעים בגרסה. אם התכונה מציינת מילון גיאו-מרחבי והקובץ רק מצייר מדידות rectilinear, זה היה ה-false positive הזה, ו-build עדכני שומר את הקובץ ללא שינוי. אם התכונה אמיתית, או מצהירים על ה-extension או נועלים לגרסה שמכילה את התוכן ביושר; העלאת הגרסה רק כדי להשתיק את השער מסתירה את השאלה האם צרכנים במורד הזרם יודעים לקרוא את מה שאתם משלחים. אותו עיקרון של בדיקות מוגבלות ומבוססות-ראיות מניע את ה-preflight של מצב כתיבה ל-PDF/E-1 עבור מסמכי הנדסה, שם שרטוטי CAD פוגשים תקן תאימות ולא מספר גרסה
בדיקות תאימות גרסה, מילוני מדידה וגיאו-מרחב, ונעילת גרסת שמירה — כולם חלק מPDF Library for Delphi, ערכת הכלים של PDFlibPas למפתחי Delphi, C++Builder ו-Lazarus