Bài viết kỹ thuật

In tài liệu PDF bằng PDFium Component trong Delphi

Tọa độ của PDF là điểm (point), tọa độ của máy in là thiết bị đơn vị, và hai thứ này chẳng có điểm gì chung cho đến khi bạn cố ý chuyển đổi chúng. Sự khác biệt này là gốc rễ gây ra phần lớn những bản in tồi từ ứng dụng Delphi: mã lệnh gửi tệp đúng nhưng trang in ra bị cắt xén (cropped), bị biến dạng (stretched) hoặc bị trắng tinh. PDFium Component xử lý mảng kết xuất (rendering) một cách gọn gàng; mảng máy in (printer plumbing) là theo tiêu chuẩn của VCL. Hai thứ này khớp với nhau bằng một lượng mã nhỏ nếu bạn hiểu yêu cầu của từng phía

Cách luồng kết xuất-rồi-in (render-then-print) hoạt động

PDFium Component không trực tiếp nói chuyện với máy in. Mô hình của nó là: kết xuất một trang thành TBitmap ở độ phân giải mà bạn muốn, sau đó chuyển bitmap đó sang canvas của máy in bằng lệnh StretchDIBits. TPdf.RenderPage trả về một bitmap thuộc sở hữu của người gọi, vì vậy bạn có thể kiểm soát kích thước pixel. Truyền [rePrinting] trong tập tùy chọn và PDFium sẽ đổi đường dẫn kết xuất của nó sang đường dẫn bỏ qua các hiệu ứng chỉ-dành-cho-màn-hình như tối ưu hóa subpixel cho LCD, và xử lý chính xác hộp phương tiện (MediaBox) của trang để xuất ra máy in. Nếu bỏ rePrinting đi, những gì bạn gửi đến máy in sẽ là hình ảnh của một màn hình, nó trông sẽ rất ổn trên màn hình máy tính nhưng sẽ tạo ra những bản in bị mờ trên các máy in có DPI cao bởi vì các quyết định tối ưu hóa cho màn hình 96 DPI không phù hợp với việc in 300 hay 600 DPI

TPdf.Active là cổng duy nhất cần kiểm tra trước khi động vào bất kỳ thuộc tính nào của trang. Thành phần này sẽ âm thầm bỏ qua các lỗi tải: việc thiết lập Active := True trên một tệp hỏng hoặc có mật khẩu bảo vệ sẽ không tung ra một ngoại lệ; nó đơn giản là để nguyên Active bằng False. Luôn kiểm tra nó sau khi thực hiện lệnh gán. Việc đọc PageCount hoặc PageWidth trên một tài liệu không hoạt động (inactive) sẽ trả về không, kết quả là những thao tác rỗng (no-ops) im lặng sẽ rất khó để chẩn đoán sau khi chúng tiến tới trình đệm in (spooler)

Một vòng lặp in tối giản

Trường hợp đơn giản nhất có thể hoạt động là tải tệp, mở tác vụ in, lặp qua các trang và đóng. Chi tiết mẹo duy nhất ở đây là Printer.NewPage không được phép gọi trước trang đầu tiên, đó là lý do tại sao có cờ FirstPage. Lệnh chuyển StretchDIBits thông qua các hàm GetDIBSizesGetDIB để lấy (pull) dữ liệu DIB (device-independent bits) từ con trỏ (handle) bitmap, sau đó tô chúng lên canvas của máy in với kích thước toàn trang:

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;

Truyền Printer.PageWidthPrinter.PageHeight làm kích thước bitmap nghĩa là bạn sẽ kết xuất ở kích thước pixel gốc của máy in, nó đã tính sẵn DPI của thiết bị. Lệnh gọi StretchDIBits sau đó ánh xạ những pixel đó với tỉ lệ 1:1 lên trang in. Việc này cho ra độ trung thực cao nhất có thể đạt được mà không cần các phép toán số học DPI tường minh, nhưng nó chỉ hoạt động khi trang PDF và tờ giấy in thực tế có cùng kích cỡ. Khi chúng khác nhau, bạn cần lấy tỉ lệ (scaling) một cách tường minh

Thay đổi kích thước khi khổ giấy in và kích thước trang khác nhau

Một trang PDF có định dạng A4 nằm dọc không tự động vừa với máy in Letter tiêu chuẩn Mỹ, và một trang nằm ngang (landscape) được gửi đến máy in đang để chiều dọc (portrait) sẽ bị cắt xén (clip). Cách tiếp cận thông thường là tính toán một tỉ lệ co dãn (scale factor) đồng nhất từ tỉ số của số lượng pixel máy in so với số điểm (point) của PDF, sau đó áp dụng vào cả hai kích thước sao cho tỉ lệ khung hình (aspect ratio) được giữ nguyên. Pdf.PageWidthPdf.PageHeight sẽ báo cáo kích thước trang hiện hành bằng các điểm (point), trong đó một point bằng 1/72 inch. Bằng cách nhân với DPI của máy in mục tiêu rồi chia cho 72, ta sẽ quy đổi ra được số lượng pixel ở độ phân giải đó. Hãy dùng Min (lấy giá trị nhỏ hơn) của hai tỉ số X và Y để ra được tỉ lệ co dãn lớn nhất mà vẫn bảo đảm hiển thị trong khu vực in:

// 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;

Việc kết xuất với Dpi = 300 là phù hợp với hầu hết các loại máy in văn phòng. Ở 600 DPI, bitmap của một trang A4 tiêu tốn khoảng 34 megapixel, tức là 100 MB với ảnh bitmap 32-bit; sự cải thiện về chất lượng đối với tài liệu văn bản thông thường là rất nhỏ trong khi chi phí bộ nhớ cho từng trang lại đáng kể. Hãy giữ 600 DPI cho các cửa hàng in ấn (print shops) hay những bản vẽ kỹ thuật nặng về vector khi điều đó thật sự cần thiết

Cờ reAnnotations trong khối mã thứ hai độc lập với rePrinting. Bao gồm nó khi người dùng mong đợi các con dấu, đánh dấu, và hộp bình luận xuất hiện trên giấy. Lược bỏ nó để xuất ra đầu ra chỉ chứa nội dung (content-only). Cả hai cờ có thể được kết hợp một cách thoải mái

Xoay trang

PDFium lưu thông tin xoay của trang trong file PDF dưới mục nhập (entry) /Rotate, có thể truy cập qua Pdf.PageRotation, nó trả về một giá trị TRotation (ro0, ro90, ro180, ro270). Hệ tọa độ của máy in sẽ làm đảo ngược các phép quay 90 và 270 độ so với trên màn hình. Nếu bạn đưa giá trị PageRotation thô trực tiếp cho RenderPage mà không có sự điều chỉnh nào, những trang nằm ngang được lồng ghép trong một tài liệu dạng chiều dọc sẽ in lộn ngược đầu ở hầu hết các driver máy in Windows. Giải pháp là đảo một lần duy nhất trước lệnh kết xuất (render): ánh xạ ro90 thành ro270ro270 ngược lại thành ro90, để ro0ro180 không thay đổi

Hãy xác minh hành vi này trên chiếc máy in đích cụ thể của bạn trước khi giao hàng (shipping). Các hành vi của driver đối với tính năng xoay trang không đồng bộ ở tất cả các nhà cung cấp, và một số driver tự thực hiện sửa góc xoay của riêng nó ở mức GDI. Nếu bạn thấy tính năng này bị nhân đôi, loại bỏ phép hoán đổi; nếu không thấy sự thay đổi nào, thì hãy thêm nó vào. Một tài liệu hỗn hợp với các trang nằm dọc và nằm ngang đan xen nhau là cách nhanh nhất để bắt được một trong hai sự cố trên lúc tiến hành chạy thử

Quản lý bộ nhớ với những tác vụ in lớn

Mỗi lệnh gọi tới RenderPage sẽ cấp phát (allocate) một TBitmap mới mà người gọi sẽ sở hữu và chịu trách nhiệm giải phóng (free). Ở trong vòng lặp trên, khối try/finally Bitmap.Free đã xử lý chính xác việc này cho một trang một lần. Tuyệt đối không được tích lũy các bitmap đó trên nhiều trang: việc kết xuất với độ phân giải 300-DPI của một tài liệu khoảng 200 trang sẽ tiêu thụ hết hàng gigabytes trước khi trang đầu tiên kịp chạy đến spooler. Hãy giải phóng từng file ảnh bitmap trước khi đi đến một trang in mới

Cặp hàm AllocMem / FreeMem ở trong đoạn chuyển (transfer) cũng tuân thủ nguyên tắc tương tự. GetDIBSizes nói cho bạn biết tiêu đề DIB (DIB header) và dữ liệu các pixel cần tổng cộng bao nhiêu bộ nhớ; bạn thực hiện việc cấp phát bộ nhớ (allocate), điền (fill), rồi tô màu (paint) và cuối cùng là giải phóng nó, tất cả đều phải nằm bên trong không gian hoạt động (scope) của trang đó. Để cho bất cứ khối nào bị rò rỉ đều sẽ khiến tác vụ in đó làm tràn cạn kiệt heap của quy trình nếu bản tài liệu có độ dài từ vài chục trang

Nếu bạn cần cho chạy lệnh in trên một luồng nền (background thread), hãy để cho TPdf và tất cả các lệnh gọi máy in của VCL (VCL printer) hoạt động cùng trên một luồng. Bản thân TPdf không hỗ trợ an toàn luồng (thread-safe) trên nhiều phiên bản (instances) nếu chúng cùng chia sẻ trạng thái toàn cục của file PDFium DLL; giải pháp an toàn nhất ở đây là sử dụng một TPdf trên mỗi luồng, và mỗi một trong số đó lại tự xử lý quá trình nạp bản gốc (loading copy) của mình

API kết xuất và tài liệu được hiển thị ở đây là một phần của PDFium Component cho Delphi và C++Builder