דף A4 יחיד המרונדר ברמת זום נוחה לקריאה תופס זיכרון של כמה מגה-בייט של מפת סיביות (bitmap) של 32 סיביות. הכפילו זאת בחוזה של 400 עמודים והמתמטיקה מפסיקה להיות מופשטת: רנדרו כל דף מראש ואתם מבקשים מווינדוס מעל ג'יגה-בייט של מפות סיביות שהמשתמש יסתכל עליהן רק מסך אחד בכל פעם. האפליקציה או שתיגמר ממרחב הכתובות בגרסת 32 סיביות או שתבלה את שניותיה הראשונות קפואה בזמן שה-GPU ומנתח הדפים מעבדים דפים שאף אחד עדיין לא גלל אליהם. מציג בגלילה רציפה חייב להרגיש כמו סרט ארוך של דפים, אך הוא אינו יכול להחזיק את כולם בזיכרון בבת אחת
מתח זה הוא הבעיה כולה כאן. רכיב PDFium פותר זאת בתוך TPdfView, כך שרוב העבודה היא בחירת מצב התצוגה הנכון והבנת מה שהרכיב עושה עבורכם. החלקים שהוא אינו עושה בשבילכם, קביעת גודל הדפים לזרימת קריאה ושמירה על גלילה מהירה ותגובתית, הם המקום שבו קוד קטן מוכיח את ערכו. אם אתם עדיין מרכיבים את המעטפת שמסביב (סרגל כלים, תמונות ממוזערות, תיבת חיפוש), המדריך למציג עשיר בתכונות מכסה תחום זה; כאן הנושא הוא הגלילה עצמה
העימוד הוא מצב תצוגה, לא פאנל של מפות סיביות
האינסטינקט מעבודת טפסי VCL הוא להגיע לתיבת גלילה (scroll box) ולערום בתוכה פקדי תמונה, אחד לכל דף. הימנעו מכך. עיצוב זה מאלץ אתכם להיות הבעלים של מיקום הדפים, חישובי הגלילה ושאלת הזיכרון בבת אחת, ותמציאו מחדש כל אחד מהם בצורה גרועה. ה-TPdfView כבר ממדל את המסמך כריצה רציפה של דפים וחושף את העימוד דרך המאפיין DisplayMode שלו
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
זהו כל מערך הגלילה הרציפה. הערך dmSingleContinuous מניח את הדפים בעמודה אנכית יחידה כאשר המרווחים ביניהם מטופלים באופן פנימי, והתצוגה נגללת דרך עמודה זו כמשטח אחד. אין פקד לכל דף שיש לחבר ואין מטפל גלילה שיש לכתוב לצורך ניווט רגיל. שימו לב לבדיקה של Pdf.Active לאחר ההקצאה: פתיחת מסמך אינה מעלה חריגה לעולם, כך שקובץ פגום או מוגן בסיסמה משאיר את Active במצב False ללא חריגה לתפיסה, ומציג המדלג על בדיקה זו מרנדר פאנל ריק ומאשים את עצמו
אותו מאפיין נושא את מצבי הפריסה הזוגיים. הערך dmTwoPageContinuous מציב דפים זה לצד זה, שניים בכל שורה, לצורך קריאה בסגנון ספר שחלק מהמסמכים דורשים; הערך dmTwoPageContinuousWithCover עושה את אותו הדבר אך מאפשר לדף הראשון לעמוד לבדו כעטיפה כך שהפריסות הבאות נופלות על גבול זוגי-אי-זוגי טבעי. כל השלושה נגללים ברציפות. המעבר ביניהם הוא הקצאה יחידה, מה שהופך תיבת בחירה של מצבי תצוגה לקלה מאוד להוספה מאוחר יותר
רק הדפים הגלויים עוברים רסטריזציה
הסיבה שזה מתאים לקובץ של 400 עמודים היא שהעמודה היא וירטואלית. ה-TPdfView יודע את הגובה של כל דף מתוך עץ הדפים של המסמך, כך שהוא יכול לחשב את היקף הגלילה הכולל ואת המיקום של כל דף מבלי לבצע רסטריזציה של דבר. רסטריזציה (rasterization), השלב היקר שהופך את זרם התוכן של הדף לפיקסלים, מתרחשת רק עבור הדפים שכרגע מצטלבים עם אזור התצוגה (viewport), בתוספת מרווח קטן כדי שדף יהיה מוכן עד שהוא נגלל לתצוגה. כשאתם גוללים מטה, דפים הנכנסים לאזור התצוגה מרונדרים ודפים העוזבים אותו משחררים את מפות הסיביות שלהם. הזיכרון נשאר יחסי למה שמתאים למסך, ולא לאורך המסמך
חשוב להפנים זאת מכיוון שזה משנה את האופן שבו אתם חושבים על עלות. פתיחת מסמך של 400 עמודים היא זולה: היא מנתחת את המבנה, לא את התוכן. העלות היא לכל דף והיא משולמת באופן עצל (lazily), ברגע שהדף נגלל לקרבת מקום. מציג שמרגיש מיידי בפתיחה וחלק בגלילה אינו עושה פחות עבודה בסך הכל, הוא פשוט פורס את העבודה על פני נתיב הקריאה בפועל של המשתמש ומשליך את מה שנשאר מאחור. התוצאה המעשית היא שכמעט אף פעם אינכם רוצים לכפות רינדור דפים לפני שהמשתמש מגיע אליהם. תנו לתצוגה להחליט מה גלוי
התאימו את הדפים לרוחב, ואז השאירו את הזום ללא שינוי
עמודת קריאה דורשת דפים המותאמים לרוחב הפאנל, ולא מקובעים לזום מוחלט. המאפיין FitMode עושה זאת וממשיך לעשות זאת כאשר החלון משנה את גודלו
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
עם pfmFitWidth הרכיב מחשב מחדש את הזום בכל פעם שהתצוגה משנה את גודלה, כך שהעמודה תמיד ממלאת את הרוחב הזמין וגובה הדפים, ולכן היקף הגלילה, נובעים מכך. יש מלכודת אחת שתופסת אנשים: הקצאת Zoom באופן ישיר מאתחלת את FitMode חזרה ל-pfmNone. זה מכוון, מכיוון שזום ידני והתאמה אוטומטית הן כוונות סותרות, אך זה אומר שהקצאה מקרית של PdfView.Zoom := 1.0 איפשהו בקוד שלכם תבטל בשקט את ההתאמה לרוחב ושינוי הגודל הבא יפסיק להשתנות בהתאם. אם אתם מציעים גם פקד זום וגם כפתור התאמה, התייחסו אליהם כאל מעבר מצב: הגדרת אחד מנקה את השני, ואתם מחליטים מי מנצח
עבור פקדי זום מוחלטים שנקראים בצורה טבעית, התצוגה חושפת את זום ההתאמה כערכים שתוכלו להחיל או להציג: PageWidthZoom[PageNumber] מחזיר את הזום שיתאים את אותו דף לרוחב, והערך PageZoom המתאים מתאים את הדף כולו. קריאת אלה היא הדרך שבה אתם מאכלסים תפריט "התאם לרוחב" / "התאם לדף" מבלי לכתוב בקוד אחוזי קסם שמשתבשים בדפי לרוחב (landscape) או בדפים גדולים במיוחד
שמרו על גלילה מהירה ותגובתית עם רינדור הדרגתי
נתיב הרינדור כברירת מחדל מצייר דף עד להשלמתו לפני שהוא חוזר. עבור דף בודד זה בסדר. במהלך גלילה מהירה במסמך צפוף זה לא: כל דף שחולף במהירות מתחיל רסטריזציה מלאה, ואם המשתמש גולל מהר יותר מכפי שדפים יכולים להתרנדר, הרינדורים הללו נערמים והפאנל מגמגם מכיוון שמתבצעת עבודה עבור דפים שכבר יצאו מהמסך עד שהיא מסתיימת. התיקון הוא להפוך את הרינדור לניתן לביטול ולנטוש אותו ברגע שהמשתמש ממשיך הלאה
הפונקציה RenderPageProgressive מרנדרת במקטעים ובודקת אסימון ביטול בכל גבול מקטע, כך שניתן לזרוק רינדור פעיל של דף שזה עתה נגלל מהמסך במקום להמשיך להריץ אותו עד הסוף
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Cancel whatever was rendering; the old token is now signaled.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
הצורה החשובה היא ערך ההחזרה. הערך prsDone פירושו שמפת הסיביות מצוירת במלואה וראויה להצגה על המסך; הערך prsCancelled פירושו שמיקום גלילה חדש יותר החליף את הדף הזה, כך שאתם משליכים את התוצאה החלקית במקום להציג אותה; הערך prsFailed הוא שגיאה אמיתית באותו דף. בדיקת הביטול מתבצעת בגבולות מקטעים ולא באופן מונע, לכן צפו לעיכוב של עשרות מילי-שניות בין קריאה ל-Cancel לבין הפסקת הרינדור בפועל. זה עדיין זול בהרבה מאשר לתת לרינדור דף מלא מיושן לחסום את התור. העברת nil כאסימון מרנדרת ישירות עד להשלמה, שהיא הבחירה הנכונה עבור רינדור חד-פעמי כמו תצוגה מקדימה להדפסה שבה אין צורך לבטל דבר
כאשר אתם קוראים לצורת הפונקציה של RenderPage במקום זאת, זו המחזירה TBitmap חדש, זכרו שהקורא הוא הבעלים שלו וחייב לשחרר אותו ב-Free. בלולאת גלילה המקצה מפת סיביות לכל דף, שכחה של שחרור זה היא דליפת זיכרון הגדלה עם כל דף שהמשתמש עובר, שזה בדיוק כשל הזיכרון הלא מוגבל שעיצוב הגלילה הרציפה אמור היה למנוע. רנדרו לתוך מפת סיביות בשימוש חוזר בכל מקום שתוכלו
מה שנשאר לכם
מציג הגלילה הרציפה הוא בעיקר באחריות הרכיב לספק. אתם בוחרים ב-dmSingleContinuous לעימוד, מגדירים את pfmFitWidth כדי שהעמודה תשתנה עם החלון, ובודקים את Pdf.Active כדי שקובץ פגום ייכשל בצורה ברורה. החלק היחיד שראוי לכתוב בעצמכם הוא רינדור הניתן לביטול, מכיוון שמציג נשפט לפי האופן שבו הוא מתנהג כאשר מישהו גורר את סרגל הגלילה לתחתית מסמך ארוך והפאנל עומד בקצב או לא. כל מה שמעבר לכך, בחירת טקסט בין דפים, הדגשת חיפוש, עץ סימניות, היא עבודת ממשק היושבת מעל משטח גלילה זה ולא בתוכו
ממשקי ה-API של TPdfView, DisplayMode ו-RenderPageProgressive המוצגים כאן הם חלק מרכיב PDFium עבור Delphi ו-Lazarus