מאמר טכני

הדפסת מסמכי PDF עם PDFium Component ב-Delphi

קואורדינטות PDF הן בנקודות, קואורדינטות מדפסת הן ביחידות התקן (device units), ולשתיים אין שום קשר אחת לשנייה עד שאתה ממיר אותן במכוון. חוסר ההתאמה הזה הוא השורש של רוב פלטי ההדפסה הגרועים ביישומי Delphi: הקוד שולח את הקובץ הנכון אבל הדף יוצא חתוך, מתוח או ריק. PDFium Component מטפל בצד הרינדור בצורה נקייה; צנרת המדפסת היא VCL סטנדרטי. השניים משתלבים יחד עם כמות צנועה של קוד ברגע שאתה מבין למה כל צד מצפה

כיצד פועל צינור הרינדור-ואז-הדפסה

PDFium Component לא מדבר עם מדפסות ישירות. התבנית היא: לרנדר דף ל-TBitmap ברזולוציה הרצויה, ואז להעביר את מפת הסיביות לקנבס המדפסת עם StretchDIBits. הפונקציה TPdf.RenderPage מחזירה מפת סיביות בבעלות הקורא, כך שאתה שולט בממדי הפיקסלים. העבר את [rePrinting] בקבוצת האפשרויות, ו-PDFium מחליף את נתיב הרינדור שלו לכזה שמשמיט אפקטים המיועדים למסך בלבד, כגון רמז תת-פיקסל של LCD (LCD subpixel hinting), ומטפל ב-MediaBox של הדף בצורה נכונה עבור פלט הדפסה. השאר את rePrinting בחוץ, ומה שאתה שולח למדפסת הוא רינדור מסך, שנראה בסדר על צג אבל נוטה לייצר פלט רך יותר במדפסות DPI גבוהות, מכיוון שהחלטות הרמזים שנעשו עבור מסכי 96 DPI אינן מתאימות להדפסה של 300 או 600 DPI

TPdf.Active הוא השער היחיד שיש לבדוק לפני שנוגעים במאפיין דף כלשהו. הרכיב בולע שגיאות טעינה בשקט: קביעת Active := True בקובץ פגום או מוגן בסיסמה לא מעלה חריגה; זה פשוט משאיר את Active כ-False. בדוק אותו תמיד לאחר ההקצאה (assignment). קריאת PageCount או PageWidth במסמך לא פעיל מחזירה אפס, מה שמייצר פעולות-סרק שקטות (silent no-ops) שקשה מאוד לאבחן ברגע שהן מגיעות לספולר (spooler)

לולאת הדפסה מינימלית

המקרה העובד הפשוט ביותר טוען קובץ, פותח משימת הדפסה, עובר על הדפים, וסוגר. הפרט הבעייתי היחיד הוא שאסור לקרוא ל-Printer.NewPage לפני הדף הראשון, ומכאן הדגל FirstPage. ההעברה ב-StretchDIBits עוברת דרך GetDIBSizes ו-GetDIB כדי למשוך ביטים בלתי תלויים בהתקן (device-independent bits) מידית מפת הסיביות, ואז מציירת אותם על קנבס המדפסת בגודל הדף המלא:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // הטעינה נכשלה בשקט; צא

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // רינדור ברזולוציית המדפסת; rePrinting מתאים את נתיב הרינדור
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

העברת Printer.PageWidth ו-Printer.PageHeight כממדי מפת הסיביות משמעותה שאתה מרנדר בגודל הפיקסלים המקורי של המדפסת, שכבר לוקח בחשבון את ה-DPI של ההתקן. לאחר מכן קריאת StretchDIBits ממפה את הפיקסלים הללו 1:1 על הדף. זה נותן לך את הנאמנות הטובה ביותר שניתן להשיג ללא כל חישובי DPI מפורשים, אך זה עובד רק כאשר דף ה-PDF והנייר הפיזי הם באותו גודל. כשהם שונים, אתה זקוק לקנה מידה מפורש

שינוי קנה מידה כאשר גודלי הדף והנייר שונים

דף PDF בגודל A4 לאורך אינו מתאים אוטומטית למדפסת בגודל US Letter, ודף לרוחב המוזן למדפסת בעלת כיוון לאורך יחתך (clip). הגישה הסטנדרטית היא לחשב יחס קנה מידה אחיד (uniform scale factor) מהיחס של פיקסלי המדפסת לנקודות PDF, ואז להחיל אותו על שני הממדים כך שיחס הגובה-רוחב יישמר. Pdf.PageWidth ו-Pdf.PageHeight חושפים את ממדי הדף הנוכחי בנקודות, כאשר נקודה אחת היא 1/72 אינץ'. הכפלה ב-DPI המטרה וחלוקה ב-72 ממירה לפיקסלים באותה רזולוציה. קח את ה-Min של יחסי ה-X וה-Y כדי לקבל את קנה המידה הגדול ביותר שעדיין נכנס לאזור הניתן להדפסה:

// התאם דף PDF לאזור הניתן להדפסה, תוך שמירה על יחס גובה-רוחב
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // רזולוציית רינדור מטרה
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // הגבל (Clamp) ל-1.0 רק עבור התאמה לכיווץ (ללא הגדלה)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... העבר עם StretchDIBits כמו למעלה
end;

רינדור ב-Dpi = 300 מתאים לרוב המדפסות המשרדיות. ב-600 DPI, מפת הסיביות עבור עמוד A4 בודד מגיעה לכ-34 מגה-פיקסל, שזה בערך 100 מגה-בייט כמפת סיביות של 32 סיביות; הרווח באיכות עבור מסמכי טקסט רגילים הוא מינימלי, ועלות הזיכרון לכל עמוד היא משמעותית. שמור את ה-600 DPI לחנויות דפוס או לשרטוטים טכניים עתירי וקטורים היכן שזה באמת משנה

הדגל reAnnotations בבלוק הקוד השני בלתי תלוי ב-rePrinting. כלול אותו כאשר המשתמש מצפה לראות חותמות, הדגשות ותיבות הערות מופיעים על הנייר. השמט אותו עבור פלט של תוכן בלבד. ניתן לשלב את שני הדגלים בחופשיות

סיבוב דף

PDFium שומר סיבוב דף ב-PDF כערך /Rotate, הנגיש דרך Pdf.PageRotation, שמחזיר ערך TRotation (ro0, ro90, ro180, ro270). מערכת הקואורדינטות של המדפסת הופכת סיבובים של 90 ו-270 מעלות ביחס למסך. אם תעביר את הערך הגולמי של PageRotation ישירות ל-RenderPage ללא כל התאמה, דפים לרוחב (landscape) המוטמעים במסמך לאורך יודפסו הפוך ברוב מנהלי ההתקן (drivers) למדפסות של Windows. התיקון הוא החלפה פשוטה לפני קריאת הרינדור: מפה את ro90 ל-ro270 ואת ro270 בחזרה ל-ro90, תוך השארת ro0 ו-ro180 ללא שינוי

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

ניהול זיכרון לאורך משימת הדפסה ארוכה

כל קריאה ל-RenderPage מקצה TBitmap חדש שהקורא הוא בעליו וחייב לשחרר. בלולאה שלמעלה, בלוק ה-try/finally Bitmap.Free מטפל בזה נכון עבור דף אחד בכל פעם. אל תצבור מפות סיביות על פני דפים: רינדור ב-300-DPI של מסמך בן 200 דפים יצרוך ג'יגה-בייטים לפני שהדף הראשון בכלל מגיע לספולר. שחרר כל מפת סיביות לפני המעבר לדף הבא

הצמד AllocMem / FreeMem בתוך בלוק ההעברה פועל לפי אותו כלל. GetDIBSizes אומר לך כמה זיכרון צריכים כותרת ה-DIB ונתוני הפיקסלים; אתה מקצה, ממלא, מצייר ומשחרר, הכל בתוך תחום הראות של דף אחד. מתן אפשרות לכל אחד מהבלוקים לדלוף (leak) יגרום למשימת ההדפסה למצות את הערימה (heap) של התהליך על מסמכים שארוכים יותר מכמה תריסרי דפים

אם עליך להריץ משימות הדפסה בחוט רקע (background thread), שמור את TPdf וכל קריאות המדפסת של VCL על אותו חוט. TPdf כשלעצמו אינו בטוח-לחוטים (thread-safe) על פני מופעים החולקים את המצב הגלובלי של ספריית ה-DLL של PDFium; המודל הבטוח ביותר הוא TPdf אחד לכל חוט, כאשר כל אחד מהם טוען עותק משלו של הקובץ

ממשק הרינדור והמסמך המוצג כאן הוא חלק מ-PDFium Component עבור Delphi ו-C++Builder