مقاله فنی

چاپ اسناد PDF با PDFium Component در دلفی

مختصات PDF به پوینت است، مختصات چاپگر برحسب واحدهای دستگاه است، و این دو هیچ ربطی به یکدیگر ندارند مگر اینکه عمداً آنها را تبدیل کنید. این عدم تطابق ریشه بیشتر خروجی‌های بد چاپ در برنامه‌های دلفی است: کد فایل مناسب را ارسال می‌کند، اما صفحه برش داده شده، کشیده یا خالی بیرون می‌آید. PDFium Component سمت رندرینگ را به طور تمیز مدیریت می‌کند؛ لوله‌کشی (plumbing) چاپگر VCL استاندارد است. هنگامی که متوجه شدید هر طرف چه انتظاری دارد، این دو با مقدار متوسطی کد با هم قرار می‌گیرند

چگونه خط لوله رندر-سپس-چاپ (render-then-print) کار می‌کند

PDFium Component به طور مستقیم با چاپگرها صحبت نمی‌کند. الگو به این صورت است: یک صفحه را روی TBitmap با رزولوشن (وضوح) دلخواه رندر کنید، سپس آن بیت‌مپ را با StretchDIBits به بوم چاپگر منتقل کنید. TPdf.RenderPage یک بیت‌مپ متعلق-به-تماس‌گیرنده را برمی‌گرداند، بنابراین شما ابعاد پیکسل را کنترل می‌کنید. [rePrinting] را در مجموعه گزینه‌ها (options) قرار دهید و PDFium مسیر رندر خود را به مسیری تغییر می‌دهد که جلوه‌های فقط-صفحه‌نمایش مانند اشاره زیرپیکسلی (subpixel hinting) LCD را حذف می‌کند، و MediaBox صفحه را به درستی برای خروجی چاپ مدیریت می‌کند. rePrinting را بیرون بگذارید و آنچه به چاپگر ارسال می‌کنید یک رندر صفحه نمایش است، که روی مانیتور خوب به نظر می‌رسد اما در چاپگرهای با DPI بالا خروجی نرم‌تری تولید می‌کند زیرا تصمیمات اشاره‌گری (hinting) که برای صفحات با رزولوشن 96 DPI گرفته می‌شود با چاپگرهای 300 یا 600 DPI سازگار نیست

TPdf.Active تنها دروازه‌ای است که قبل از دست زدن به ویژگی صفحه بررسی می‌شود. این کامپوننت خطاهای بارگذاری را بی‌صدا می‌بلعد: تنظیم Active := True روی یک فایل آسیب‌دیده یا محافظت‌شده با رمز عبور باعث ایجاد استثنا نمی‌شود؛ صرفاً Active را به صورت False رها می‌کند. همیشه بعد از واگذاری (assignment) آن را بررسی کنید. خواندن PageCount یا PageWidth در یک سند غیرفعال (inactive) صفر را برمی‌گرداند، که باعث تولید no-op‌های بی‌صدا می‌شود که پس از رسیدن به قرقره (spooler) تشخیص آن‌ها بسیار سخت است

یک حلقه چاپ مینیمال

ساده‌ترین حالت کاری یک فایل را بارگیری می‌کند، یک کار چاپی (print job) باز می‌کند، صفحات را تکرار می‌کند و می‌بندد. تنها جزئیات پیچیده این است که Printer.NewPage نباید قبل از صفحه اول فراخوانی شود، از این رو پرچم FirstPage وجود دارد. انتقال StretchDIBits از طریق GetDIBSizes و GetDIB برای کشیدن بیت‌های مستقل از دستگاه از دسته (handle) بیت‌مپ می‌رود، سپس آنها را روی بوم چاپگر در اندازه تمام صفحه نقاشی می‌کند:

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;  // load failed silently; bail out

    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;

        // Render at printer resolution; rePrinting adjusts the render path
        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 در صفحه نقشه (map) می‌کند. این به شما بهترین وفاداری (fidelity) قابل دستیابی را بدون هیچ‌گونه محاسبات DPI صریح (explicit DPI arithmetic) می‌دهد، اما تنها زمانی کار می‌کند که صفحه PDF و کاغذ فیزیکی از نظر اندازه یکسان باشند. زمانی که متفاوت باشند، به مقیاس‌گذاری صریح (explicit scaling) نیاز دارید

مقیاس‌گذاری در مواقعی که اندازه‌های صفحه و کاغذ متفاوت است

یک صفحه PDF در پرتره A4 به طور خودکار با چاپگر Letter ایالات متحده متناسب نیست و یک صفحه افقی (landscape) که به یک چاپگر عمودی (portrait-oriented) داده می‌شود، بریده (clip) می‌شود. رویکرد استاندارد محاسبه ضریب مقیاس یکنواخت (uniform scale factor) از نسبت پیکسل‌های چاپگر به نقاط (points) PDF است، سپس آن را در هر دو بعد اعمال می‌کند تا نسبت تصویر (aspect ratio) حفظ شود. Pdf.PageWidth و Pdf.PageHeight ابعاد صفحه فعلی را در نقاطی (points) نشان می‌دهند، جایی که یک پوینت 1/72 اینچ است. ضرب در DPI هدف و تقسیم بر 72 تبدیل به پیکسل‌ها در آن رزولوشن می‌شود. از نسبت‌های X و Y گزینه Min را انتخاب کنید تا بزرگترین مقیاسی را که هنوز در ناحیه قابل چاپ (printable area) قرار می‌گیرد، به دست آورید:

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

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

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  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]);
  // ... transfer with StretchDIBits as above
end;

رندر با رزولوشن Dpi = 300 برای اکثر چاپگرهای اداری مناسب است. در رزولوشن 600 DPI، بیت‌مپ برای یک صفحه A4 منفرد به حدود 34 مگاپیکسل می‌رسد که به صورت یک بیت‌مپ 32-بیتی حدود 100 مگابایت است؛ دستاورد کیفیت برای اسناد متنی معمولی حداقل است و هزینه حافظه در هر صفحه قابل توجه است. رزولوشن 600 DPI را برای چاپخانه‌ها یا نقشه‌های فنی پر از وکتور (vector-heavy) که در آن‌ها واقعاً اهمیت دارد، نگه دارید

پرچم (flag) reAnnotations در بلوک کد دوم مستقل از rePrinting است. هنگامی که کاربر انتظار دارد مهرها (stamps)، هایلایت‌ها و کادرهای نظر روی کاغذ ظاهر شوند، آن را وارد کنید. برای خروجی فقط محتوا، آن را حذف کنید. هر دو پرچم را می‌توان آزادانه ترکیب کرد

چرخش صفحه

PDFium چرخش صفحه را در PDF به عنوان یک مدخل (entry) /Rotate ذخیره می‌کند که از طریق Pdf.PageRotation قابل دسترسی است، که مقدار TRotation (مانند ro0، ro90، ro180، ro270) را برمی‌گرداند. سیستم مختصات چاپگر، چرخش‌های 90 و 270 درجه را نسبت به صفحه نمایش وارونه (inverts) می‌کند. اگر مقدار خام PageRotation را مستقیماً به RenderPage بدون هیچ‌گونه تنظیم بفرستید، صفحات افقی (landscape) جاسازی شده در یک سند پرتره (portrait) در اکثر درایورهای چاپگر ویندوز، به صورت وارونه چاپ می‌شوند. راه حل یک جابجایی (swap) ساده قبل از فراخوانی رندر است: ro90 را به ro270 و ro270 را دوباره به ro90 نگاشت (map) کنید، در حالی که ro0 و ro180 را بدون تغییر بگذارید

این رفتار را در چاپگر هدف خاص خود قبل از ارسال (shipping) تأیید کنید. رفتار درایور حول چرخش در بین فروشندگان یکنواخت نیست، و برخی از درایورها تصحیح چرخش خاص خود را در سطح GDI اعمال می‌کنند. اگر چرخش مضاعف (double rotation) مشاهده کردید، تعویض (swap) را بردارید؛ اگر اصلاً اصلاحی ندیدید، آن را اضافه کنید. یک سند با جهت‌گیری-مختلط (mixed-orientation) با صفحات متناوب پرتره و افقی، سریع‌ترین راه برای درک هر یک از حالت‌های شکست (failure mode) در طول آزمایش است

مدیریت حافظه در طول یک کار چاپی طولانی

هر فراخوانی به RenderPage یک TBitmap جدید را اختصاص می‌دهد که تماس‌گیرنده صاحب آن است و باید آزاد (free) کند. در حلقه بالا، بلوک try/finally Bitmap.Free این کار را برای یک صفحه در یک زمان به درستی انجام می‌دهد. بیت‌مپ‌ها را در سراسر صفحات انباشته (accumulate) نکنید: یک رندر 300-DPI از یک سند 200 صفحه‌ای قبل از اینکه صفحه اول به قرقره (spooler) برسد، گیگابایت‌ها را مصرف می‌کند. هر بیت‌مپ را قبل از رفتن به صفحه بعدی آزاد کنید

جفت AllocMem / FreeMem در داخل بلوک انتقال (transfer) از همان قانون پیروی می‌کند. GetDIBSizes به شما می‌گوید هدر DIB و داده‌های پیکسلی به چه مقدار حافظه نیاز دارند؛ شما همه را در محدوده یک صفحه تخصیص می‌دهید، پر می‌کنید، نقاشی می‌کنید و آزاد (free) می‌کنید. اجازه دادن به نشت هر یک از بلوک‌ها باعث می‌شود کار چاپی هرم (heap) فرآیند (process) را در اسناد طولانی‌تر از چند ده صفحه به پایان برساند

اگر نیاز به اجرای کارهای چاپی در یک رشته (thread) پس‌زمینه دارید، TPdf و همه فراخوانی‌های چاپگر VCL را روی یک رشته (thread) نگه دارید. TPdf به خودی خود در میان نمونه‌هایی که به اشتراک می‌گذارند، به حالت سراسری (global state) PDFium DLL امن برای-رشته (thread-safe) نیست؛ ایمن‌ترین مدل یک TPdf در هر رشته (thread) است که هر کدام کپی مخصوص به خود را از فایل بارگیری می‌کنند

API رندرینگ و سند که در اینجا نشان داده شده است، بخشی از PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر (C++Builder) است