מאמר טכני

תצוגה מקדימה להדפסה ופלט Device-Context של PDFlibPas ב-Delphi

רינדור של דף PDF על גבי device context של Windows לצורך תצוגה מקדימה להדפסה מכניס שלושה מערכות קואורדינטות לאותה שורת קוד, והן לעולם אינן מסכימות. דף ה-PDF נמדד בנקודות כשהמקור בפינה השמאלית-תחתונה. ה-DC של המסך נמדד בפיקסלים כשהמקור בפינה השמאלית-עליונה ועם מקדם זום שבוחרים בעצמנו. ה-DC של המדפסת, זה שהתצוגה המקדימה אמורה לחזות, מודד פיקסלים ברזולוציית ההתקן אך ממקם את המקור בפינת אזור ההדפסה, לא בפינת הגיליון. שגיאה באחד מהם גורמת לכך שהתצוגה המקדימה נראית טוב בעוד שהדף המודפס יוצא עם הזזה, שינוי קנה מידה, או גזיזה לאורך קצה. התסמין הנפוץ הוא טופס עם גבול שמוצג ממורכז בתצוגה המקדימה ומודפס עם כללי עליון ושמאלי חתוכים, כי המדפסת הלייזר אינה יכולה להדפיס בכמה המילימטרים החיצוניים ואיש לא עדכן את התצוגה המקדימה על כך. losLab PDF Library (PDFlibPas) מכסה את כל הנתיב עם קריאות רינדור ל-device-context, שכבת קינפוג מדפסת וירטואלית, ותמונות bitmap לתצוגה מקדימה שנוצרות ממדדי המדפסת עצמה — וזה החלק שגורם לתצוגה המקדימה להיות כנה לגבי אותו שוליים

גיאומטריית הנייר אינה גיאומטריית ההדפסה

שני מלבנים מתארים כל יעד הדפסה, והיסט ביניהם הוא המקום שבו רוב באגי התצוגה המקדימה מתגוררים. מלבן הנייר הוא הגיליון הפיזי. מלבן ההדפסה הוא האזור הקטן יותר שמנוע ההדפסה יכול בפועל להגיע אליו, מוסט פנימה בשוליים חומרתיים שונים בכל דגם מדפסת ולעיתים בכל מגש. שכבת ההדפסה של הספרייה מודדת את שניהם. מחלקת TPLPrinter הבסיסית חושפת את PageWidth ו-PageHeight עבור אזור ההדפסה, את FullPageWidth ו-FullPageHeight עבור הגיליון המלא, ואת PrintOffsetX עם PrintOffsetY עבור הפרש בין המקורות, הכל בפיקסלים של ההתקן ברזולוציה שמדווח GetDPI. תצוגה מקדימה כנה מקטינה את אותן מספרים לרזולוציית המסך במקום לצייר את הדף לתוך כל מלבן שקורה להיות לפקד. דילוג על שלב זה גורם לתצוגה המקדימה להניח שוליים אפסיים בשקט, וזה בדיוק הערך שאף מדפסת אמיתית אינה משתמשת בו

תצוגה מקדימה על המסך דרך RenderPageToDC

עבור פקד תצוגה מקדימה על המסך, RenderPageToDC(DPI, Page, DC) מצייר דף של המסמך הטעון ישירות על כל device context של GDI, בין אם מדובר בקנבס של TPaintBox, bitmap מחוץ למסך, או DC של metafile. הארגומנט DPI קובע את הזום. 96 מקרב תצוגה של 100% על מסך קלאסי, והכפלתו מכפילה את גודל הרינדור

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // these three are sticky library state, not per-call parameters:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

המלכודת היא שנתיב הרינדור ל-DC מונהג על ידי מצב מצטבר של הספרייה, לא על ידי פרמטרים לכל קריאה. SetRenderDCOffset, SetRenderDCErasePage, ו-SetRenderCropType כל אחד מתמיד עד שמשהו משנה אותם, כך שלולאת תמונות ממוזערות שרצה לאחר שהמשתמש כיוון את תצוגת הזום יורשת כל הסטה או גזיזה שנתיב הקוד הקודם השאיר אחריו. התסמין הוא תצוגה מקדימה שסוטה רק בסדרות ניווט ספציפיות, מה שהופך את הבאג לקשה לשחזור במיוחד. קביעת כל המצב הרלוונטי בראש מטפל הציור, כמו לעיל, לא עולה כלום ומסלקת את כל הסוג הזה. כפל נסתר שני מסתתר בסמוך. הרזולוציה האפקטיבית של הפלט היא קנה מידה הרינדור כפול הארגומנט DPI, ואמנם SetRenderScale ברירת המחדל שלו היא 1.0, אך הוא גם מתמיד לאחר שינוי, כך שתכונת ייצוא שהגדילה אותו בשקט מקטינה מחדש כל תצוגה מקדימה עתידית עד שמשהו מאפס אותו

לצופים בגלילה ורינדורים חלקיים יש גרסה ייעודית. RenderPageToDCClip מקבל מפרט חיתוך יחד עם device context, כך שביטול תוקף של רצועה אחת של החלון מצייר מחדש רק את אותה רצועה במקום לרסטר מחדש את הדף המלא. בזום גבוה בדפים בפורמט גדול זה ההבדל בין צופה שעוקב אחר סרגל הגלילה לאחד שמטשטש מאחוריו

עבודת הדפסה התואמת את התצוגה המקדימה

צד ההדפסה עובד דרך מדפסת וירטואלית. NewCustomPrinter משכפל מדפסת מערכת לתצורה פרטית של הספרייה, ו-SetupPrinter מכוון את אותו שיבוט מבלי לגעת ב-DevMode ברמת המחשב: נייר מוזן כהגדרה 1 (קבוע DMPAPER_*) וכיוון כהגדרה 11. התמורה היא בידוד. שירות יכול להדפיס תוויות A4 בעוד שמדפסת ברירת המחדל של המארח נשארת על Letter, וכלום לא צריך שחזור לאחר מכן

var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // setting 1 = paper, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // setting 11 = orientation, 1 = portrait
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // fit to paper, auto-rotate + center
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

PrintOptions ראוי לקריאה קפדנית. הוא מחזיר handle של אפשרויות שחייבים להעביר ל-PrintDocument או ל-PrintPages; הוא אינו מצב רקע. בניית האפשרויות ושכחת העברת ה-handle נכשלת בשקט. העבודה מודפסת עם ברירות מחדל, ואיש אינו שם לב עד שמדיניות התאמה לנייר צופה ודף גדול מדי יצא חתוך במקומה. ארגומנט קנה המידה של הדפים הוא המקום שבו אותה מדיניות נמצאת. ללא שינוי קנה מידה נשמרת הדיוק המימדי, שחשוב לטפסים שנמדדים מול סרגל. התאמה לנייר משנה קנה מידה של הכל לגיליון. כיווץ דפים גדולים משאיר דפים רגילים כפי שהם ומתערב רק כאשר דף חורג מאזור ההדפסה, שהוא בדרך כלל ברירת המחדל הנכונה לערכת מסמכים מעורבת. דגל הסיבוב-ומרכוז האוטומטי מטפל בדפי נוף ללא נתיב קוד נוסף

יישומים שכבר מנהלים TPrinter דרך תיבת הדו-שיח VCL יכולים להעביר אותו ישירות. PrintDocumentToPrinterObject ו-PrintPagesToPrinterObject מקבלים את מופע TPrinter המוגדר, מה שמשאיר את תיבת הדו-שיח הסטנדרטית להדפסה כמשטח הגדרות לפני המשתמש בעוד שהספרייה מטפלת ברינדור הדפים. ערבוב שתי הגישות בנתיב קוד אחד נוטה לחדש מחדש את דרייפט הגיאומטריה שכל שאר העבודה הזו אמורה לבטל, אז בחרו אחת. נתיב המדפסת הווירטואלית מתאים לשירותים ללא השגחה; נתיב ה-TPrinter מתאים ליישומים אינטראקטיביים

פלט סלקטיבי עובד באותו אופן. PrintPages מקבל מחרוזת טווח, אז העברת שם המדפסת הווירטואלית, '2-5,12', וה-handle של האפשרויות מדפיסה דפים 2 עד 5 ו-12 עם חוזה הגיאומטריה שלם, ואותה תחביר מניע את גרסאות ההדפסה לקובץ. אותן גרסאות קובץ הן התשובה המעשית לסביבה ללא השגחה ללא התקן פיזי מחובר: בדיקת רגרסיה של גיאומטריית הדפסה על שרת בנייה שאין לו תור מנהלי התקנים כלל. מרנדרים את אותו מסמך דרך אותן אפשרויות לקובץ artifact בכל בנייה, ורגרסיית גיאומטריה הופכת ל-diff במקום לדוח לקוח שלושה שבועות מאוחר יותר

bitmap של תצוגה מקדימה עם מדדי המדפסת עצמה

תצוגה מקדימה שרונדרה ב-96 DPI כנגד גודל דף מונח עונה על השאלה הלא נכונה. היא מראה איך הדף נראה, לא מה המדפסת הזאת תניח על הנייר הזה. GetPrintPreviewBitmapToString סוגר את הפער הזה על ידי בניית התצוגה המקדימה מאותה מדפסת מותאמת אישית ואותו handle של אפשרויות כמו העבודה הסופית, כך שגודל הנייר, הכיוון, מדיניות שינוי קנה המידה, הסיבוב, וההסטה החומרתית כולם מוזנים ל-bitmap. מה שמוחזר הוא מה שהגיליון יראה

procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

הארגומנט MaxDimension מגביל את הצלע הארוכה של ה-bitmap. 1200 פיקסלים נשאר חד לתיבת דו-שיח תצוגה מקדימה ומשמר צריכת זיכרון צנועה גם לשרטוטי הנדסה בפורמט E, שבהם רינדור ברזולוציה מלאה ברזולוציית 600 DPI של המדפסת היה מגיע לגיגה-בייטים

זכירת בחירות המדפסת של המשתמש

תיבות דו-שיח להדפסה ששוכחות את הגדרותיהן בין הפעלות יוצרות פניות תמיכה משלהן. זוג ה-DevMode, GetPrinterDevModeToString ו-SetPrinterDevModeFromString, מסדר את תצורת מנהל ההתקן המלאה של מדפסת למחרוזת אטומה שאפשר לאחסן בהעדפות המשתמש ולשחזר בהפעלה הבאה, כולל האפשרויות הספציפיות למנהל ההתקן שאף API גנרי אינו טורח לדגם. שמרו את המדפסת לפי שם מ-GetPrinterNames, אף פעם לא לפי אינדקס ברשימה. סדר האינדקסים משתנה בכל פעם שמדפסת מתווספת או מוסרת, כך שאינדקס שמור מצביע בשקט על ההתקן הלא נכון בפעם הבאה שהרשימה משתנה. GetDefaultPrinterName מכסה את הנפילה-חזרה כאשר ההתקן שנזכר נעלם לחלוטין

בחירת המגש משלימה את סיפור הצמדה. GetPrinterBins מדווח על מקורות הנייר שמנהל ההתקן חושף, שחשוב לזרימות עבודה של נייר כותרת שבהן דף ראשון שולף מהמגש של נייר הכותרת והשאר מהמגש של נייר רגיל. זו מדיניות שמשתמשים מצפים שהיישום ייזכר לצד כל השאר, ועבודת הדפסה שנוחתת על הנייר הלא נכון נקראת כבאג גם כאשר כל בייט ב-PDF היה נכון

שמרו על מנוע אחד לרוחב התצוגה המקדימה וההדפסה

החלטה אחרונה שולטת בשקט על הנאמנות. בחירת מנוע הרינדור חלה הן על יעדי המסך והן על יעדי המדפסת, כך שהפיתוי הוא לצפות בתצוגה מקדימה עם מנוע מהיר ולהדפיס עם מנוע מדויק. התנגדו לכך. הפעלת התצוגה המקדימה והעבודה דרך מנועים שונים מחדשת בדיוק את דרייפט הנאמנות שתצוגה מקדימה נאמנה למדפסת נבנתה לסלק, ועושה זאת בדרך שמופיעה רק על הנייר. הפשרות בין מנועי ה-built-in, Cairo, ו-PDFium נשקלות ברינדור PDF מרובה-מנועים ב-Delphi; בחרו אחד והשתמשו בו משני הצדדים

מסמכים גדולים מדי לטעינה נוחה לפני הדפסה ניתן לפתוח דרך נתיב הגישה הישיר המתואר במיזוג, פיצול וגישה ישירה ל-PDF גדולים, שמרנדר דפים ל-device context מ-file handle ללא בניית עץ המסמך. מפרט API ההדפסה המלא נמצא בדף המוצר losLab PDF Library for Delphi