Bài viết Kỹ thuật

Xây dựng Trình xem PDF Tương tác trong Delphi với TPDFlibViewer

Tích hợp một khung đọc PDF vào ứng dụng doanh nghiệp Delphi không đòi hỏi bạn phải viết vòng lặp dựng hình (rendering loop), công cụ cuộn trang (scroll engine) hoặc mô hình thu phóng (zoom model). losLab PDF Library cung cấp sẵn TPDFlibViewer, một control VCL giúp mở tài liệu chỉ với một dòng lệnh và tích hợp sẵn các hành vi như chọn văn bản, cuộn trang kéo thả (panning), thu phóng khu vực (marquee zoom), chụp ảnh màn hình (snapshots), tô sáng vùng, tô sáng tìm kiếm toàn văn, tương tác với chú thích và in ấn. Bài viết này sẽ hướng dẫn năm bước để biến một form trống thành một trình xem PDF hoạt động hoàn chỉnh: kéo thả control, liên kết các action sẵn có, thêm tô sáng tìm kiếm, xử lý click vào chú thích và in ấn

Tại sao nên sử dụng control trình xem thay vì tự viết vòng lặp dựng hình?

Lập luận về phân lớp (layering) được đưa ra đầu tiên, bởi vì nó giải thích những gì bạn thực sự nhận được. Dựng hình một trang thành một bitmap chỉ chiếm 20 phần trăm dễ dàng; 80 phần trăm còn lại là tất cả những gì bao quanh nó: bộ nhớ đệm bitmap trang (page-bitmap cache) với dung lượng giới hạn, bố cục cuộn trang trên các trang có kích thước khác nhau, các chế độ thu phóng tự động tính toán lại khi thay đổi kích thước, kiểm tra va chạm (hit-testing) giúp ánh xạ điểm pixel chuột ngược trở lại tọa độ trang ở bất kỳ mức thu phóng và góc xoay nào, và lập lịch vẽ lại (repaint scheduling) không bị nhấp nháy. TPDFlibViewer tự xử lý tất cả các tác vụ đó và ủy quyền công việc raster cho cùng một engine được mô tả trong bài viết về dựng hình đa engine, nhờ đó lớp điều khiển (control layer) và lớp dựng hình (rendering layer) có thể được thay thế độc lập. Code của form giao tiếp với một Windows control có thể cuộn được, chứ không phải với một renderer

Làm thế nào để tích hợp một trình xem PDF vào ứng dụng Delphi?

TPDFlibViewer.LoadFromFile mở tài liệu thông qua instance TPDFlib nội bộ của chính control và trả về 1 nếu thành công, vì vậy trình xem tối giản nhất chỉ cần một lệnh gọi constructor, gán Parent và một lệnh load. Nếu bạn đã có sẵn một instance TPDFlib ở nơi khác trong ứng dụng, AttachLibrary sẽ hiển thị tài liệu được chọn của nó thay thế, với quyền sở hữu vẫn thuộc về phía bạn

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // pan by dragging; a plain click still follows links
end;

Property ViewerMode dùng để chọn chức năng của chuột, bao gồm năm giá trị: vmSelect là công cụ chọn văn bản mặc định, vmHand giúp cuộn trang bằng cách kéo chuột với con trỏ hình bàn tay trong khi một click thông thường vẫn có thể theo liên kết, vmZoom là tính năng thu phóng khu vực (marquee zoom) giúp phóng to vùng được kéo để lấp đầy viewport, vmSnapshot chụp vùng được kéo vào clipboard (với sự kiện OnSnapshot và switch SnapshotToClipboard), và vmHighlight vẽ các vùng tô sáng mờ liên tục. Công cụ tô sáng này rất đáng lưu ý về mặt tọa độ: các vùng được lưu trữ trong không gian người dùng của trang (page user space), không phải pixel của client, vì vậy các đánh dấu từ AddHighlightRegion vẫn tồn tại khi thu phóng, xoay chế độ xem và chuyển hướng trang mà bạn không cần phải tự quản lý. Trong một chế độ xem được xoay, hướng kéo thậm chí có thể ngược lại với các trục của trang, điều này tạo ra chiều rộng hoặc chiều cao âm sau khi ánh xạ xoay ngược — control sẽ chuẩn hóa hình chữ nhật đó cho bạn

Làm thế nào để liên kết thanh công cụ PDF trong Delphi mà không cần code trung gian (glue code)?

Unit PDFlibViewerActions loại bỏ hoàn toàn code trung gian cho thanh công cụ. Nó cung cấp sẵn hai chục class kế thừa từ TAction bao gồm chuyển hướng trang (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), thu phóng (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), xoay chế độ xem (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), in ấn, các dialog mở file và lưu dưới dạng (save-as), copy văn bản được chọn, và một action chuyển đổi cho mỗi công cụ chuột (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode). Mỗi action trỏ tới một control thông qua property Viewer được publish và duy trì trạng thái của riêng nó: mỗi action tự disable khi không có tài liệu nào được mở, các action chuyển hướng trang theo dõi vị trí trang, copy theo dõi xem có vùng chọn nào tồn tại không, và các action chế độ cũng như khớp kích thước giữ cho trạng thái check của chúng đồng bộ với công cụ đang hoạt động và chế độ thu phóng. Trong lúc thiết kế (design time), tất cả chúng đều đăng ký trong danh mục losLab PDF của trình chỉnh sửa Action List; lúc chạy (runtime), các class tương tự chỉ cần ba dòng code cho mỗi cái

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

Có một cái bẫy ở phạm vi unit đáng lưu ý nếu bạn tự xây dựng việc đăng ký action trên mô hình này. RegisterActions được khai báo trong System.Actions, chứ không phải trong Vcl.ActnList, và lỗi trình biên dịch (compiler error) bạn nhận được khi chỉ có Vcl.ActnList trong mệnh đề uses sẽ gây hiểu nhầm rất lớn: literal mảng class [TAction1, TAction2, ...] sẽ bị phân tích cú pháp ngược thành một set, tạo ra một chuỗi lỗi E2010 Incompatible types: 'Integer' and 'class of ...' trông giống như sự không tương thích về kiểu trong các class action của bạn. Cách khắc phục là thêm một uses tương ứng, nhưng chẩn đoán lỗi lại chỉ ra mọi nơi ngoại trừ chỗ đó. Đối với thanh công cụ thu phóng, TPDFlibViewer.EffectiveZoomPercent báo cáo tỷ lệ thu phóng thực tế đang có hiệu lực — bao gồm cả hệ số được tính toán trong các chế độ fit-width và fit-page — vì vậy một bước thu phóng có thể dựa trên độ phóng to thực tế hiện tại chứ không phải giá trị cuối cùng bạn gán

Tô sáng kết quả tìm kiếm và việc đảo ngược tọa độ đằng sau nó

HighlightSearchHits tìm kiếm toàn bộ tài liệu cho một truy vấn và vẽ một thanh mờ đè lên mọi kết quả khớp, trả về số lần khớp; ClearSearchHighlights, SearchHighlightCount và property SearchHighlightColor hoàn thiện tính năng này. Bởi vì các hình chữ nhật kết quả khớp được lưu trữ trong không gian trang và được chiếu lại trên mỗi lần vẽ, các thanh này vẫn được dán chặt vào văn bản của chúng khi người dùng cuộn, thu phóng và xoay chế độ xem. Nếu bạn sử dụng TPDFlibSearchPanel đi kèm, RunSearch của nó sẽ tự động vẽ các thanh tương tự trong trình xem được đính kèm thông qua property HighlightInView, vốn được bật theo mặc định

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // case-insensitive
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

Phần tinh tế, và là lý do nên ưu tiên sử dụng đường dẫn tích hợp sẵn thay vì tự xây dựng từ các kết quả tìm kiếm thô, chính là việc chuyển đổi hệ tọa độ. Engine văn bản báo cáo các hình chữ nhật kết quả khớp theo tọa độ phụ thuộc vào cài đặt Origin của thư viện: với Origin 0 hoặc 3, trục Y chạy từ dưới lên như trong không gian người dùng PDF gốc, và với Origin 2 hoặc 3, trục X được đo từ cạnh phải. Lớp phủ của trình xem hoạt động trong không gian trang từ trên xuống dưới, căn trái, vì vậy mỗi hình chữ nhật kết quả khớp phải được đảo ngược (Y = PageHeight - Bottom, và ở những nơi áp dụng X = PageWidth - Right) trước khi nó được lưu trữ — việc lấy trực tiếp cạnh trên được báo cáo theo origin mặc định sẽ vẽ tô sáng ở vị trí đối xứng theo chiều dọc trên trang. TPDFlibViewer thực hiện chuyển đổi này dựa trên origin đang hoạt động khi ghi lại từng kết quả khớp, sau đó áp dụng ánh xạ xoay thuận tại thời điểm vẽ, đó là lý do tại sao các thanh tô sáng nằm chính xác ở mọi góc độ. Bản thân độ mờ sử dụng Windows API AlphaBlend với một alpha nguồn không đổi, vì VCL canvas không có tính năng trộn alpha (alpha compositing) gốc. Các API tìm kiếm văn bản và hình học cơ bản được trình bày chuyên sâu trong bài viết về tìm kiếm văn bản và liệt kê phần tử trang

Click chú thích, gợi ý (hint) và xuất file đính kèm

TPDFlibViewer coi các chú thích (annotation) là các đối tượng tương tác hạng nhất. Click vào một chú thích không phải link sẽ kích hoạt sự kiện OnAnnotClick với số trang, chỉ mục chú thích bắt đầu từ 1 và chuỗi kiểu con (subtype); AnnotAtPagePoint hiển thị cùng một hit test theo lập trình và bỏ qua các chú thích Popup đi kèm để bạn luôn nhận được chú thích mà người dùng thực sự nhìn thấy. Di chuột qua một chú thích sẽ hiển thị văn bản Contents của nó dưới dạng hint của control thông qua switch AnnotHints, được bật theo mặc định và hoạt động cùng với property ShowHint tiêu chuẩn của VCL. Đối với các chú thích FileAttachment, SaveAnnotAttachmentToFile ghi file nhúng vào đĩa và trả về 1 nếu thành công, giúp hoàn thành quy trình click-để-lưu chỉ trong vài dòng code

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

In ấn và những giới hạn bạn nên lập kế hoạch

In ấn không cần tích hợp riêng biệt. PrintDoc hiển thị dialog in tiêu chuẩn của Windows — lựa chọn máy in, phạm vi trang, số bản in, thứ tự sắp xếp (collation) — và chuyển giao công việc cho engine in ấn của thư viện, trong khi PrintDoc(False) gửi toàn bộ tài liệu đến máy in mặc định mà không hiển thị UI nào cả. PrintDocRange hoạt động hoàn toàn theo lập trình với tên máy in rõ ràng, trang bắt đầu và kết thúc, số bản in và cờ sắp xếp, đây là điểm đầu vào phù hợp cho các tác vụ in hàng loạt hoặc in do server kích hoạt được thúc đẩy từ chính instance trình xem đó

Có hai giới hạn xứng đáng được đề cập một cách trung thực. Thứ nhất, TPDFlibViewer là một Windows VCL control; lớp trình xem bị loại trừ khỏi các bản build NOVCL, vì vậy các service console và target không phải VCL nên sử dụng trực tiếp API dựng hình TPDFlib cơ bản. Thứ hai, việc sử dụng không đầu (headless) được hỗ trợ có chủ đích nhưng bị giới hạn: CaptureRegion dựng bất kỳ vùng client nào — các trang kèm theo tô sáng — vào một bitmap do bên gọi sở hữu và hoạt động trên một control không có parent, không có window handle, bởi vì đường dẫn vẽ tránh các lệnh gọi VCL buộc phải tạo handle. Điều đó làm cho việc xác minh cấp độ pixel trong các bài kiểm tra tự động trở nên thực tế, nhưng bất cứ điều gì thực sự cần một vòng lặp thông báo (message loop), chẳng hạn như các công cụ chuột tương tác, vẫn thuộc về một phiên UI thực sự. Về mặt hiệu năng, control duy trì bộ nhớ đệm trang với giới hạn có thể định cấu hình thông qua SetCacheLimit và có thể lưu trữ các trang đã dựng qua các phiên; bài viết về bộ nhớ đệm trang trên đĩa của trình xem trình bày chi tiết về lớp đó và hành vi DPI trên mỗi màn hình của nó

Tóm lại là năm bước: đặt control và gọi LoadFromFile, đính kèm các action sẵn có từ PDFlibViewerActions, gọi HighlightSearchHits để tìm kiếm trong trang, xử lý OnAnnotClick cho quy trình chú thích, và hiển thị PrintDoc. Mỗi thành phần được hiển thị ở đây đều đi kèm trong losLab PDF Library tiêu chuẩn dành cho Delphi, nơi trình xem control, các class action và dựng hình engine được cài đặt cùng nhau