HotPDF מרנדר עמוד PDF טעון לתוך TBitmap של דלפי באמצעות קריאה אחת: RenderLoadedPageToBitmap(PageIndex, DPI). הפונקציה מפרשת את זרם התוכן של העמוד ומחזירה תמונת סיבית (bitmap) מסוג RGB ב-24 סיביות בבעלות הקורא וברזולוציה שתבחרו, שזה בדיוק מה שסרגל תמונות ממוזערות (thumbnails), תצוגה מקדימה להדפסה או צינור ייצוא מ-PDF לתמונה צריכים. מאמר זה מלווה את ה-API, ולאחר מכן את החלק המפריד בין מרנדר שמיש לבין צעצוע: ציור טקסט מתוך תוכניות הגופנים המוטמעות עצמן במקום מגופני מערכת דומים
מדוע רינדור עמוד PDF קשה יותר מציור תמונה?
עמוד PDF אינו תמונה. זו תוכנית: זרם של אופרטורים שבונים נתיבים, בוחרים גופנים, מגדירים צבעים וממקמים גליפים, המורצים מול מודל הגרפיקה המוגדר בתקן ISO 32000-1 §8. שום דבר בקובץ אינו מתאר כיצד נראה פיקסל כלשהו. כדי לייצר תמונת סיבית עליכם להריץ את התוכנית הזו — לתחזק מטריצת טרנספורמציה נוכחית, מחסנית מצב גרפי (graphics-state stack) עבור q/Q, נתיב חיתוך (clipping path), ומרחבי צבע למילוי וקו — ולבצע רסטריזציה לתוצאה. לכן "פשוט להציג את עמוד 3 כתמונה" דורש מפרש זרם תוכן, ולא המרת פורמט קובץ
המרנדר של HotPDF, שהוצג בגרסה v2.253.0, בנוי משש יחידות נפרדות המשקפות מודל זה: ליבת מטריצה אפינית לאלגברת הטרנספורמציה של PDF [a b c d e f], מחסנית מצב גרפי, פותר מרחבי צבע (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), בונה נתיבים המקשר בין אופרטורי נתיבים של PDF ל-GDI, שכבת מדדי גופנים הקוראת מערכי /Widths להתקדמות נכונה, והמפרש שמנתב אופרטורים ומפעיל את החמישה האחרים. אובייקטי תמונה (XObjects) עוברים דרך אותה מחסנית פענוח שבה הספרייה משתמשת לחילוץ, כך שכל מסנן תמונות ש-HotPDF יכול לפענח לצורך חילוץ — כולל תמונות JPEG 2000 דחוסות ב-JPXDecode — מופיע גם בפלט המרונדר
רינדור עמוד טעון ל-TBitmap
RenderLoadedPageToBitmap מקבל אינדקס עמוד מבוסס אפס וערך DPI, כאשר 72 DPI מפה יחידת מרחב משתמש אחת של PDF לפיקסל אחד. הוא מחזיר nil במקרה של כשל (אינדקס מחוץ לטווח, משאבים חסרים) במקום להעלות חריגה, כך שמציג המסמכים יכול לדלג על עמוד פגום ולהמשיך. הקורא הוא הבעלים של תמונת הסיבית המוחזרת ועליו לשחרר אותה
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('report.pdf') > 0 then
begin
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144); // page 1 at 144 DPI
if Bmp <> nil then
try
Image1.Picture.Assign(Bmp);
finally
Bmp.Free; // caller owns the bitmap
end;
end;
finally
Pdf.Free;
end;
end;
ארגומנט ה-DPI מבצע את עבודת השינוי בקנה מידה (scaling) עבור כל תרחיש נפוץ. סרגל תמונות ממוזערות מרונדר ב-36 או 48 DPI ומקבל תמונות סיבית קטנות ומהירות; תצוגה מקדימה על המסך ב-96 או 144 DPI מתאימה לצפיפות התצוגה הטיפוסית; ונתיב ייצוא ב-300 DPI מייצר תמונות באיכות הדפסה. סיבוב עמוד מערך ה-/Rotate והיפוך המקור של /MediaBox (קובצי PDF שמים את נקונד הקורדינטות השמאלית התחתונה, GDI בפינה השמאלית העליונה) מנוהלים בתוך מטריצת העמוד-למכשיר, כך שעמוד US Letter ב-72 DPI חוזר כ-612x792 פיקסלים בדיוק בכיוון הנכון
מדוע תמונות ממוזערות של PDF מרונדרות מציגות גליפים שגויים?
גליפים שגויים או מקורבים בפלט PDF מרונדר פירושם כמעט תמיד שהמרנדר מחליף את הגופן בגופן מערכת במקום להשתמש בגופן המוטמע בקובץ. המרנדר הראשון של HotPDF עשה בדיוק את זה: הוא הסיר את קידומת תת-הקבוצה מ-/BaseFont (הפך את ABCDEF+Arial ל-Arial), ביקש מ-GDI גופן מערכת בשם זה, וצייר את הטקסט בעזרתו. עבור מסמך המשתמש ב-Arial או ב-Times New Roman ... התוצאה נראית קרובה. אך מדובר בקירוב בלבד, והוא נשבר בדרכים מוגדרות היטב
גופנים מוטמעים של תת-קבוצות (subset-embedded fonts) הם המקרה הגרוע ביותר. גופן תת-קבוצה עשוי לשאת רק את ארבעים הגליפים שהמסמך משתמש בהם בפועל, כאשר קודי התווים מוקצים בסדר פרטי לקובץ זה — קוד 1 עשוי להיות "T", קוד 2 "h" וכדומה. גופן מערכת אינו יודע דבר על הקצאה פרטית זו, כך שהטקסט נעלם או מופיע כתווים שגויים לחלוטין. קידודים מותאמים אישית, גופני סמלים, גופני ברקוד וכל גופן שאינו מותקן במכונת הרינדור נכשלים באותו אופן. מרנדר העוצר בהחלפת גופן מערכת מייצר תמונות ממוזערות שניתן לזהות בהן את העמוד — עד שהעמוד משתמש בגופנים שהפכו את ההטמעה להכרחית מלכתחילה
רינדור גליפים מוטמעים: ציור מתוך תוכנית הגופן עצמה
HotPDF סגר את הפער הזה לאורך חמש גרסאות (מגרסה v2.268.0 עד v2.272.0) על ידי פענוח תוכניות הגופנים המוטמעות ושחזור קווי מתאר של גליפים שלהן כנתיבי וקטור GDI מלאים. טקסט בעמוד מרונדר מגיע כעת מאותם נתוני קווי מתאר שבהם משתמש מציג תואם, מה שאומר שגופני תת-קבוצה, קידודים מותאמים אישית וגופנים שאינם מותקנים מתרנדרים בצורתם המדויקת. הכיסוי נבנה לפי סוג הגופן:
עבור גופני Type0/CIDFontType2 עם תוכנית TrueType מוטמעת (FontFile2), המרנדר מפענח את טבלאות glyf ו-loca ישירות: קווי מתאר ריבועיים (quadratic contours) מומרים לעקומות בזייה (Béziers) מעוקבות ש-GDI מבין, נקודות מרומזות על הקו בין נקודות עוקבות מחוץ לקו משוחזרות, וגליפים מורכבים משוחזרים באופן רקורסיבי. נתמכות הן פריסות Identity והן פריסות CIDToGIDMap מפורשות של זרמים, והתקדמות ה-CID מכבדת את ערכי הרוחב /W ו-/DW width, כך שטקסט Identity-H של שני בתים מתקדם כראוי
תוכניות CFF (מזהה FontFile3, בין אם CIDFontType0C, Type1C או מעטפת OpenType) מקבלות מפרש charstring מלא של Type 2: קווים, עקומות, משפחת ה-flex, מסכות רמזים (hint masks) וקריאות שגרת משנה מקומיות/גלובליות עם הטיית שגרת המשנה הנכונה. תוכניות CFF מבוססות CID מפות קודי תווים דרך ערכת התווים של הגופן, מה שחשוב עבור גופני תת-קבוצה שסדר הגליפים שלהם שונה מסדר ה-CID, ובחירת מילון הגופנים (font-DICT) לכל גליף דרך FDArray/FDSelect מכובדת. גופני TrueType פשוטים (שאינם CID) פותרים קודים של בית אחד דרך טבלת ה-cmap של הגופן המוטמע עצמו עם שרשרת תת-טבלאות חסונה — פורמטים של Unicode 4 ו-12 תחילה, לאחר מכן תת-טבלאות של סמלים עם מראת השימוש הפרטי F000, ולבסוף פורמטים מיושנים של מקינטוש — בעוד שגופני Type1 פשוטים נפתרים דרך הקידוד המובנה של תוכנית ה-CFF
שני שיפורים משלימים את התמונה. ראשית, מילוני /Encoding של גופנים פשוטים נפתרים לפי סדר העדיפויות שקובע תקן ISO 32000-1 §9.6.6: מערכי /Differences עוקפים את קידוד הבסיס, שעוקף את המפה של תוכנית הגופן עצמה — הנתיב שבו תלויות מערכות כלים מבוססות TeX ו-PostScript, כאשר שמות גליפים נפתרים דרך Adobe Glyph List, ערכת תווים CFF או ה-cmap של TrueType. שנית, גופני Type3, שהגליפים שלהם הם בעצמם זרמי תוכן קטנים, משוחזרים דרך המרנדר בשילוב מטריצת הגופן, גודל הגופן ומטריצת הטקסט; ערכי /Widths במרחב הגליף מפורשים דרך ה-/FontMatrix כפי שדורש תקן ISO 32000-1 §9.6.5, ופרוצדורות גליפים המצהירות על תיבה חוסמת (bounding box) מסוג d1 נחתכות לפיה, כך שגליף ברקוד פגום אינו יכול לצייר מחוץ לתא שלו. כאשר לא ניתן למפות קוד — תוכנית פגומה, תו לא ממופה — המרנדר נסוג לציור גופן מערכת עבור אותו גליף במקום להשמיט את כל קטע הטקסט
כיצד הופכים רינדורים חוזרים למהירים?
התשובה ש-HotPDF מספק היא מטמון עמודים בשימוש האחרון (most-recently-used): RenderLoadedPageToBitmapCached שומר עד RenderCacheCapacity עמודים מרונדרים (ברירת מחדל 8) המזוהים לפי אינדקס עמוד ו-DPI, ופגיעה במטמון (cache hit) מחזירה עותק חדש בבעלות הקורא מבלי לגעת בזרם התוכן — בדרך כלל פי אלפים מהיר יותר מפרשנות מחדש של העמוד. דפוס זה מתאים למציגי מסמכים בדיוק: משתמש העובר בין שני עמודים, או אירוע שינוי גודל המבקש שוב את אותו עמוד באותו DPI, פוגע במטמון בכל פעם
// Thumbnail strip: first pass renders, scrolling back hits the cache
for I := 0 to ThumbCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
if Bmp <> nil then
try
ThumbList.AddThumbnail(I, Bmp);
finally
Bmp.Free;
end;
end;
// After editing a loaded page in place:
Pdf.InvalidateRenderedPageCache; // next render reflects the change
היו מודעים לעלות הזיכרון לפני העלאת הקיבולת. עמוד US Letter ב-300 DPI הוא 2550x3300 פיקסלים, כ-25MB כתמונת סיבית ב-24 סיביות, כך ששמונה עמודים שמורים במטמון ברזולוציית ייצוא מחזיקים כ-200MB. ב-DPI של תמונות ממוזערות אותן שמונה רשומות עולות הרבה פחות ממגה-בייט. התאימו את RenderCacheCapacity ל-DPI שבו אתם שומרים במטמון בפועל, וקראו ל-InvalidateRenderedPageCache לאחר כל עריכה במקום — המטמון מבוסס על עמוד ו-DPI בלבד, והוא אינו יכול לדעת שהתוכן שמתחת השתנה. טעינת מסמך חדש מנקה אותו אוטומטית
מטמון שני פועל מתחת למטמון העמודים: אובייקטי תמונה (XObjects) מפוענחים נשמרים במאגר בעל תקציב בתים המוגבל על ידי ImageCacheMaxBytes (ברירת מחדל 32MB) עם פינוי לפי שימוש אחרון. לוגו או כותרת עליונה החוזרים על עצמם בכל עמוד מפוענחים פעם אחת לכל טעינת מסמך במקום פעם אחת לכל אופרטור Do, מה שמקצץ בערך בחצי את זמן הרינדור לעמודים בעלי תמונות משותפות ומאיץ את הייצוא ל-TIFF מרובה עמודים באותו שיעור. InvalidateRenderedPageCache מנקה גם את המטמון הזה
מה שעדיין מרונדר בקירוב
מרחבי צבע מסוג CalRGB, Lab ומרחבי צבע מבוססי ICC מיוצגים בקירוב ולא מנוהלים צבעונית — מרחבי צבע של מכשירים, פלטות אינדקס וחיפושי צבע של פונקציית Type 0 דגומה מטופלים, אך קובץ הפקה להדפסה המסתמך על כוונות רינדור של ICC לא יהיה מדויק קולורימטרית. דפוסי הצללה (sh) ומצבי מיזוג מעבר לאלפא פשוטה נמצאים אף הם מחוץ לטווח, ורקורסיית Form XObject מוגבלת בעומק כהגנה מפני לולאות. עבור חשבוניות, דוחות, חוזים וטפסים — עמודים העשויים מטקסט, נתיבים ותמונות — הפלט נאמן למקור; עבור הוכחת עיצוב (design proof) מלאה במעברי צבע וקבוצות שקיפות, התייחסו לתמונת הסיבית כתצוגה מקדימה בלבד, ולא כהוכחה סופית
המשמעות המעשית: אם צינור העבודה שלכם מייצר מסמכים עם HotPDF או צורך קובצי PDF עסקיים טיפוסיים, RenderLoadedPageToBitmap round-trips them with the exact embedded glyph shapes, correct CID advances, and correct page geometry. הקירובים קיימים בפינות של מודל הגרפיקה שמסמכים עסקיים ממעטים לבקר בהן
RenderLoadedPageToBitmap, הגרסה השמורה במטמון שלו, וצינור רינדור הגליפים המוטמעים המתואר כאן מסופקים כחלק מ-רכיב HotPDF עבור דלפי ו-C++Builder — ספריית VCL טבעית ללא תלות בקובצי DLL חיצוניים, המכסה יצירת PDF, עריכה, חילוץ טקסט ורינדור עמודים בחבילה אחת