PDFlibPas פותר תווים שהגופן הנבחר אינו מסוגל לצייר על ידי חיפוש בשרשרת גיבוי של גופנים מותקנים, אשכול אחר אשכול, תוך שמירה על העיצוב (shaping) ועל סדר הריצות הדו-כיווניות. מפעילים זאת עם SetAutomaticFontFallback, מרחיבים את השרשרת עם AddFontFallback, ורק גופני הגיבוי שבאמת שימשו לפלט מוטמעים בקובץ
הבעיה שהיא פותרת היא כזו שכל מחולל מסמכים נתקל בה בפעם הראשונה ששם לקוח מגיע בכתב שגופן התבנית מעולם לא צפה מראש. הכשל שקט, וזה מה שהופך אותו ליקר
מדוע טקסט לא נתמך נעלם במקום להעלות שגיאה?
מפני של-PDF אין מושג של גופן שלא יכול לצייר תו. גופן פשוט ממפה קודי בייט לשמות גליפים דרך קידוד; גופן מורכב ממפה קודים דרך CMap לאינדקסים של גליפים. בקש גליף שהגופן אינו מכיל ותקבל אינדקס גליף אפס, .notdef, שרוב הגופנים מציירים כאין־כלום או כתיבה ריקה. הקובץ תקין מבחינה מבנית, אופרטור הטקסט תקין, והעמוד נרנדר. הוא פשוט ריק במקום שבו אמור להיות השם
שום דבר ב-ISO 32000-1 לא מחייב מחולל לשים לב לכך. מחולל שכותב טקסט מבלי לבדוק כיסוי מפיק PDF תקין טכנית שאיבד תוכן בשקט, והאובדן צף על מסך הלקוח שבועות מאוחר יותר. זו הסיבה שתכונת הגיבוי ודוח הגליפים החסרים משולחים יחד: פתרון מה שניתן לפתור הוא רק חצי מהעבודה, ודיווח על מה שלא ניתן היה לפתור הוא החצי השני
הגיבוי מתבצע לפי אשכול, לא לפי נקודת קוד
הגרנולריות היא הפרט שמפריד בין מימוש עובד למימוש שנראה סביר בלבד. טקסט אינו רצף של תווים בלתי תלויים. הברה בדוואנגרי, אימוג'י עם מתאם גוון עור, אות בסיס עם סימני צירוף — כל אחד מהם הוא אשכול אחד שחייב להיות מעובד על ידי גופן אחד, מפני שהחלטות העיצוב בתוכו תלויות בטבלאות של אותו גופן
PDFlibPas פותר אשכולות, כך שאשכול שגופן גיבוי מכסה מצויר כולו על ידי אותו גופן. פיצול באמצע אשכול וציור מחצית מהגופן הראשי ומחצית מגיבוי היה מפיק תוצאה נוכחת טכנית אך שבורה חזותית, שאולי גרועה יותר מהריק שממנו התחלת. גם סדר הריצה נשמר, כך שגיבוי בתוך ריצה מימין-לשמאל אינו משנה את סדר הטקסט הסובב; אותו מנגנון עומד בבסיס הפריסה האנכית המתוארת בכתיבה אנכית ביפנית ובסינית
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// סדר החיפוש: ההתאמה הראשונה מנצחת, לכן שים את הגופנים הרחבים ביותר בסוף
Lib.AddFontFallback('Microsoft YaHei'); // סינית פשוטה
Lib.AddFontFallback('Meiryo'); // יפנית
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = הטמע את הגופן
Lib.SetTextSize(11);
Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
Lib.DrawText(72, 700, 'Delivery status: on time');
Lib.SaveToFile('invoice.pdf');
finally
Lib.Free;
end;
end;
סדר את השרשרת בכוונה. הפתרון בוחר בגופן הראשון שמכסה את האשכול, כך שגופן פאן-יוניקוד רחב שממוקם ראשון ינצח כמעט הכל, והגופנים הספציפיים לכתב שבחרת בקפידה לעולם לא ייבדקו. שים את הגופנים הספציפיים ראשונים ואת הגופן הכוללני אחרון
דיווח או ביטול: איזה כשל אתה מעדיף?
SetMissingGlyphPolicy מקבל את PDF_MISSING_GLYPH_REPORT, ברירת המחדל התואמת, או את PDF_MISSING_GLYPH_ABORT. תחת מדיניות הדיווח פעולת הטקסט ממשיכה, נקודות קוד שלא ניתן לפתור נשמטות כמו קודם, וכל אחת נרשמת. תחת מדיניות הביטול פעולת הטקסט נדחית לפני שנכתב כל תוכן, ו-LastErrorCode מוגדר ל-521
בחר לפי ייעוד המסמך. מנה של דוחות פנימיים צריכה להמשיך להיות מעובדת ולרשום את הפערים ביומן, מפני שדוח חסר מעט היום עדיף על אין דוח כלל. חוזה מחייב מבחינה משפטית, חשבונית, או כל דבר שיש עליו שם צריך להיפסק, מפני שתו שנשמט בשקט בשם צד הוא פגם שעדיף לגלות בתהליך שלך מאשר בסכסוך. מדיניות הביטול נכשלת לפני הכתיבה, כך שלא נשאר מאחור זרם תוכן חצי-מוגמר
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... build the document ...
if Lib.DrawText(72, 660, CustomerName) <> 1 then
if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
begin
Report := Lib.GetMissingGlyphReportJSON;
// {"valid":false,"policy":1,"eventCount":1,"events":[
// {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
// "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
// "fontType":"TrueType","operation":"DrawText"}]}
EscalateToOperator(Report);
end;
finally
Lib.Free;
end;
end;
הדוח קריא-מכונה ותחום בכוונה. כל אירוע נושא את העמוד, את האינדקס ב-UTF-16 בתוך המחרוזת, את נקודת הקוד הן בצורה מספרית והן בצורה U+XXXX, את הגופן שנבחר, את סוגו ואת הפעולה שנתקלה בבעיה, כך שקריאת תמיכה יכולה לנקוב בתו המדויק במקום לתאר תסמין. המעקב שומר את 256 האירועים האחרונים, שזה מספיק לאבחון מסמך וקטן מספיק כדי שריצה פתולוגית לא תוכל להפוך אבחון לבעיית זיכרון
מדידה וציור חייבים להסכים
מדידת רוחב משתמשת באותן החלטות גיבוי מודעות-אשכול כמו הציור עצמו. זה נשמע מובן מאליו וזה בדיוק מה שרוב שכבות הגיבוי הביתיות טועות בו: הן מתקנות את נתיב הציור, משאירות את המדידה על הגופן הראשי, וכל תיבת טקסט, יישור לימין ועמודת טבלה מסתיימים מחושבים מרוחבים שלא תואמים למה שנרנדר בפועל
מפני ששני הנתיבים חולקים את אותו פתרון, מחרוזת שנמדדה לפני הציור תופסת את הרוחב שבו נמדדה, כולל ריצות הגיבוי. זה מה שהופך את הגיבוי לבטוח להפעלה גלובלית ולא רק במקומות שבדקת ידנית
רק מה שהשתמשת בו מוטמע
גופני גיבוי מוטמעים באופן עצל: גופן בשרשרת שמעולם לא פתר אשכול לא תורם דבר לפלט. מסמך המכיל תו סיני אחד ו-5,000 תווים לטיניים אינו נושא גופן CJK מלא; הוא נושא את מה שמעבר תת-הקבוצה (subsetting) הפיק עבור אותו גליף בודד, וזו ההתנהגות המתוארת באופטימיזציית גודל קובץ ותת-קבוצות גופנים
עצלנות זו הופכת שרשרת רחבה לזולה בהגדרה. רשום את הגופנים שקבוצת המסמכים שלך עשויה להזדקק להם בכל שפה שאתה משרת, וכל PDF בודד משלם רק עבור מה שבאמת השתמש בו. עבור מסמכים שלא יצרת, שבהם הגופנים החסרים כבר נמצאים בתוך קובץ קיים, נתיב התיקון שונה ומכוסה בהטמעת גופנים חסרים בתוך PDF קיים
אזהרת פריסה אחת ראויה לציון מפורש: הגיבוי נפתר מול גופנים המותקנים על המכונה שמריצה את הקוד. לשרת ללא גופני CJK מותקנים אין למה לגבות, והדוח יודיע לך על כך בקובץ הראשון ולא אחרי התלונה הראשונה. שלח את הגופנים שאתה תלוי בהם, ואשר רישוי להטמעתם
PDFlibPas היא ספריית PDF ל-Delphi, ל-C++Builder ול-Lazarus עם ממשקי DLL ו-ActiveX תואמים, כך שממשקי הגיבוי ודוח הגליפים החסרים זמינים גם לקוראים שאינם Pascal. תיעוד מלא נמצא בדף ספריית PDFlibPas ל-Delphi