Bài viết kỹ thuật

Chuyển đổi RTF sang PDF trong Delphi với losLab PDF Library

RTF đã tồn tại đủ lâu để nó xuất hiện ở những nơi mà không ai tính trước: các trình tạo báo cáo (report generators) cũ, các đường ống trộn thư (mail merge pipelines), các kho lưu trữ tài liệu pháp lý có từ trước các trình xử lý văn bản hiện đại. Việc chuyển đổi nó sang PDF một cách tức thời (on the fly) là một yêu cầu thường xuyên, và cách tiếp cận thực sự hiệu quả trên Windows không phải là một trình phân tích cú pháp RTF chuyên dụng, mà là đường dẫn kết xuất do chính Windows cung cấp thông qua TRichEditEM_FORMATRANGE. Phiên bản losLab PDF Library DLL phơi bày một bối cảnh thiết bị ảo (virtual device context) cắm trực tiếp vào đường ống đó

Cơ chế: DC ảo và EM_FORMATRANGE

Các điều khiển (controls) Rich Edit có thể phân trang nội dung của chúng cho bất kỳ bối cảnh thiết bị (device context) nào, không chỉ dành cho một máy in vật lý. Thông điệp EM_FORMATRANGE báo cho điều khiển đó biết phải bố trí một dải ký tự vào trong một DC nhất định và trả về vị trí của ký tự cuối cùng mà nó đã xoay xở để khớp vừa vặn. Gọi nó lặp đi lặp lại, đẩy cpMin tiến lên ở mỗi lần, và bạn sẽ có được đầu ra từng trang một. GetCanvasDC của losLab PDF Library cung cấp một DC trong bộ nhớ được định cỡ bằng bất kỳ kích thước trang nào bạn chỉ định; sau khi kết xuất một trang vào trong đó, LoadFromCanvasDc sẽ chụp lại kết quả dưới dạng một trang PDF. Đó là toàn bộ đường ống

Có một điều cần phải làm đúng ngay từ đầu: điều khiển TRichEdit phải được định cỡ để khớp với trang đích. Nếu điều khiển nhỏ hơn hoặc lớn hơn kích thước của DC, quá trình phân trang sẽ không thẳng hàng với những gì kết thúc trong file PDF. Đối với đầu ra khổ A4, cách tiếp cận tiêu chuẩn là thiết lập kích thước pixel của điều khiển sao cho khớp với 210 x 297 mm ở 96 DPI trước khi tải tệp RTF, bằng cách sử dụng các trình trợ giúp tỷ lệ (scale helpers) giống với những trình mà bạn sẽ sử dụng để định cỡ cho DC

Triển khai Delphi

Đoạn sau đây sử dụng import unit PDFlibAX_TLB, giúp bao bọc phiên bản DLL của thư viện. Biểu mẫu chứa một TRichEdit và một button (nút); bộ xử lý OnCreate của biểu mẫu sẽ định cỡ điều khiển và tải RTF, và thao tác click button sẽ thúc đẩy vòng lặp chuyển đổi

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // Size the control to A4 at screen DPI so pagination matches the DC
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // Obtain a virtual DC sized to A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Render the next page of RTF content into the DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Capture the DC contents as a PDF document
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // signals last page
end;

end.

Vòng lặp đang làm gì

PrintRtfBox điền vào cấu trúc TFormatRange và truyền nó cho điều khiển Rich Edit thông qua SendMessage. Điều khiển sẽ kết xuất các ký tự bắt đầu từ cpMin, dừng lại khi DC đầy, và trả về vị trí của ký tự đầu tiên không vừa vặn. Khi giá trị trả về bằng hoặc vượt quá tổng chiều dài của văn bản, mọi ký tự đã được kết xuất và hàm sẽ trả về giá trị không (zero), điều này sẽ chấm dứt vòng lặp repeat...until

Mỗi lần lặp (iteration) tạo ra một tệp PDF có tên là Output1.pdf, Output2.pdf, và cứ như thế. Nếu thay vào đó bạn muốn một tài liệu nhiều trang duy nhất, API nối-trang (page-append) của thư viện sẽ cho phép bạn lắp ráp chúng lại sau đó, hoặc bạn có thể cấu trúc lại vòng lặp để gọi AddPage bên trong một phiên làm việc duy nhất của tài liệu. Mô hình mỗi lần lặp một SaveToFile theo sau bởi RemovePdfDocument ở trên giúp giữ cho bộ nhớ đỉnh điểm (peak memory) được giới hạn ở lượng nội dung của một trang, điều này có ý nghĩa rất lớn đối với các tệp RTF dài

Các chi tiết định cỡ làm mọi người vấp váp

Tham số 96 DPI truyền cho LoadFromCanvasDc báo cho thư viện biết DC đã được kết xuất ở độ phân giải màn hình nào, để nó có thể tính toán chính xác ánh xạ từ point sang pixel cho trang PDF. Nếu làm sai điều này thì văn bản sẽ xuất hiện sai kích thước ở đầu ra mặc dù hình ảnh trông có vẻ chính xác trên màn hình

Con số +100 được thêm vào RcPage.RightRcPage.Bottom là một mức lề nhỏ vượt quá cạnh nhìn thấy được của điều khiển. Rich Edit sử dụng hình chữ nhật rcPage để quyết định nơi phân chia các trang; nếu không có lề này, một dòng nằm chính xác tại ranh giới có thể bị nhân đôi trên cả hai trang. Nó không phải là một hằng số kỳ diệu: bạn muốn nó đủ lớn để ranh giới trang rơi một cách sạch sẽ vào bên trong khu vực bố cục của điều khiển thay vì rơi vào điểm pixel cuối cùng

Cuối cùng, điều khiển đã phải được gắn vào một cửa sổ biểu mẫu nhìn thấy được khi FormCreate chạy, như vậy window handle của nó sẽ hợp lệ trước lệnh gọi đầu tiên tới SendMessage. Một TRichEdit được tạo động tại thời gian chạy (runtime) cần một lệnh gọi HandleNeeded tường minh trước khi vòng lặp kết xuất bắt đầu nếu như biểu mẫu đó vẫn chưa được hiển thị

Xử lý font chữ và các tính năng RTF

Vì việc kết xuất được thực hiện bởi engine Windows Rich Edit, việc thay thế font chữ tuân theo cùng các quy tắc mà nó sử dụng để hiển thị và in ấn. Các font chữ được tham chiếu trong tệp RTF mà đã được cài đặt trên máy sẽ kết xuất một cách trung thực; các font chữ bị thiếu sẽ được thay thế một cách âm thầm, điều này có thể làm thay đổi độ dài dòng và việc phân trang. Đối với quá trình chuyển đổi hàng loạt (batch conversion) trên sản xuất, điều này đáng để được kiểm tra một cách tường minh: hãy tải một tài liệu với từng kiểu chữ mà các nguồn RTF của bạn sử dụng và xác nhận xem số lượng trang ở đầu ra có khớp với những gì bạn mong đợi từ bản xem trước khi in thủ công hay không

Các bảng biểu, hình ảnh nhúng, và hầu hết các tính năng định dạng Rich Text sẽ hoạt động mà không cần xử lý thêm vì Rich Edit tự kết xuất chúng. Vùng duy nhất có thể gây ngạc nhiên là văn bản sử dụng khoảng cách đoạn tùy chỉnh hoặc thụt lề dòng đầu tiên được biểu thị bằng twips: hệ tọa độ nội bộ của Rich Edit là bằng twips (1/1440 inch), trong khi các tọa độ DC mà bạn thiết lập trong TFormatRange là tính bằng pixel ở mức DPI hiện tại. Điều khiển tự động chuyển đổi ở bên trong, nhưng nếu bạn đang xây dựng RTF bằng phương pháp lập trình thì bạn nên xác minh rằng các giá trị lề của bạn đang ở đúng đơn vị

DPI awareness (nhận thức DPI) và màn hình độ phân giải cao (high-DPI)

Trên màn hình đang chạy ở mức tỷ lệ thu phóng 150% (144 DPI), ScaleX(210, mmPixel) sẽ trả về một số lượng điểm pixel lớn hơn so với trên màn hình 100%. PDF Library ghi nhận bất kỳ kích thước pixel nào mà bạn truyền cho GetCanvasDC và sử dụng tham số DPI trong LoadFromCanvasDc để tính ngược lại kích thước vật lý của trang trong file PDF. Miễn là giá trị DPI bạn truyền vào khớp với mức DPI mà ứng dụng của bạn đang chạy, kích thước trang đầu ra sẽ chính xác bất kể mức thu phóng của màn hình là bao nhiêu

Nếu ứng dụng của bạn không nhận thức được DPI (DPI-unaware - giá trị mặc định cũ), Windows sẽ thu phóng screen DC và các phép tính toán pixel của bạn sẽ bị sai trên các máy high-DPI. Bản sửa lỗi đơn giản nhất là khai báo trạng thái nhận biết DPI (DPI awareness) trong tệp manifest của ứng dụng; sau đó ứng dụng sẽ nhận được các điểm pixel thiết bị thực (true device pixels) và con số 96 bạn truyền cho LoadFromCanvasDc nên được thay thế bằng mức DPI hiển thị thực tế thu được từ GetDeviceCaps(GetDC(0), LOGPIXELSX). Đoạn mã mẫu ở trên được viết cứng là 96 vì nó phù hợp cho môi trường thu phóng 100% và giữ cho ví dụ được ngắn gọn

Cấu trúc đầu ra: một tệp cho mỗi trang so với một tài liệu kết hợp

Vòng lặp ở trên ghi lại mỗi trang vào một tệp PDF riêng biệt. Việc đó có phải là thứ bạn muốn hay không phụ thuộc vào mục đích sử dụng ở khâu tiếp theo. Các hệ thống tạo báo cáo thường cần các trang riêng lẻ bởi vì sau đó chúng sẽ lắp ráp tài liệu cuối cùng lại với nhau bằng cách gộp hoặc sắp xếp lại thứ tự các trang. Nếu bạn muốn một tệp PDF duy nhất ngay từ đầu, thư viện cho phép bạn tạo một tài liệu có nhiều trang trong một phiên duy nhất: hãy tạo tài liệu một lần ở bên ngoài vòng lặp, gọi phương thức thêm-trang thay vì SaveToFile ở bên trong vòng lặp, và lưu lại toàn bộ tài liệu sau khi vòng lặp thoát ra. Điều này giúp tránh được các tệp trung gian và là một cấu trúc đúng đắn cho hầu hết các tình huống chuyển đổi tài liệu-duy-nhất

Đối với các tệp RTF lớn, bạn nên thêm một số phản hồi tiến trình bên trong vòng lặp, bởi vì tốc độ chuyển đổi tỷ lệ thuận tương đối với số lượng trang và một tài liệu 200 trang có thể mất vài giây. Cấu trúc repeat...until rất dễ để mở rộng: theo dõi khoảng dịch chuyển (offset) của ký tự bên trong thanh cập nhật tiến trình (progress bar) sau mỗi lần lặp, sử dụng LastChar chia cho tổng số đếm ký tự thu được từ RichEdit1.GetTextLen

Các phương thức GetCanvasDCLoadFromCanvasDc được hiển thị ở đây là một phần của losLab PDF Library cho Delphi và C++Builder