Bài viết kỹ thuật

So sánh PDF cạnh nhau trong Delphi với PDFium Component

Hai tài liệu mở cùng lúc, cùng số trang, mỗi tài liệu nằm trong bảng điều khiển có thể cuộn riêng biệt: đó là cốt lõi của trình xem so sánh. PDFium Component cung cấp điều này thông qua một mô hình đối tượng đơn giản nơi TPdf sở hữu tệp và TPdfView sở hữu hiển thị. Một tài liệu, một TPdf, một TPdfView. Bạn muốn ba bảng điều khiển, bạn có ba cặp. Phần khó không phải là các lệnh gọi API; mà là phép toán bố cục khi cửa sổ thay đổi kích thước và logic đồng bộ trang khi bạn quyết định dạng xem nào nên theo dạng xem nào

Bố cục Form

Form VCL chứa ba vùng chứa TScrollBox nằm cạnh nhau, mỗi vùng chứa một TPdfView bên trong và được căn chỉnh thành alClient để lấp đầy hộp. Hai thành phần TSplitter nằm giữa các hộp để người dùng có thể điều chỉnh chiều rộng cột trong thời gian chạy. Một thanh công cụ (toolbar) phía trên các bảng điều khiển chứa các nút mở, điều khiển thu phóng, và công tắc chuyển đổi chế độ hai/ba dạng xem

Chế độ ba dạng xem (Three-view mode) là một biến boolean mà form theo dõi nội bộ. Khi nó thay đổi, bạn tính toán lại chiều rộng và hiển thị hoặc ẩn cột thứ ba. Cách tiếp cận đơn giản nhất là xóa tất cả các thuộc tính Align, ẩn các bộ chia (splitter), sau đó thiết lập các vị trí tuyệt đối:

procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Việc thiết lập Align := alNone trên cả ba hộp trước khi tính toán số nguyên giúp tránh việc công cụ ràng buộc của VCL xung đột với các phép gán của bạn. Khôi phục khả năng hiển thị của bộ chia sau khi định vị nếu bạn muốn kéo-để-thay-đổi-kích-thước (drag-to-resize) trong chế độ hai dạng xem

Chiều cao của mỗi hộp cuộn (scroll box) là vùng client trừ đi chiều cao của bảng thanh công cụ. Vì thanh công cụ được neo ở trên cùng với alTop, ClientHeight - PanelButtons.Height mang lại cho bạn không gian chiều dọc có thể sử dụng. Gán giá trị này cho cả ba hộp bên trong cùng một lệnh gọi UpdateLayout để không bao giờ có một khung hình nào mà một hộp cao hơn các hộp khác và gây ra hiện tượng nhấp nháy bố cục

Mở một tài liệu

Mỗi cặp bảng điều khiển cần thủ tục mở riêng. Mẫu trình tự khá ngắn: hủy kích hoạt thành phần, thiết lập tên tệp, kích hoạt, sau đó kiểm tra Active; nếu nó vẫn là False, hãy nhắc nhập mật khẩu và thử lại. Lưu ý rằng TPdfView.Active là những gì kiểm soát việc kết xuất, nhưng TPdf.Active mới là những gì thực sự mở tệp; chúng độc lập với nhau. Việc thiết lập PdfView.Active := True khi TPdf được liên kết của nó chưa hoạt động là vô hại nhưng không hiển thị gì cả

procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // Load failures are silent: Active stays False instead of raising.
  if not PdfComponent.Active then
  begin
    // Most likely a password-protected file; give the user one retry.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

Luôn kiểm tra PdfComponent.Active sau phép gán; một tệp bị hỏng hoặc sai mật khẩu sẽ khiến quá trình tải thất bại trong im lặng mà không phát sinh ngoại lệ (exception) trong luồng mặc định. Việc thiết lập PdfViewComponent.PageNumber := 1 một cách rõ ràng sau khi mở thành công giúp tránh một số trang cũ kỹ từ tài liệu trước đó

Hộp thoại thông báo ở cuối là có chủ ý: bạn muốn các tệp bị hỏng hoặc không được hỗ trợ lộ diện ngay lập tức thay vì bị nuốt chửng như một bảng điều khiển trống rỗng, im lặng. Một người dùng không thấy gì sẽ không biết liệu tệp đã được tải và chỉ là trống trơn, hay liệu thành phần đã từ chối nó. Việc báo cáo thất bại giữ cho lỗi được hiển thị rõ ràng

Theo dõi bảng điều khiển hoạt động (Active Panel)

Khi người dùng nhấp vào bên trong một bảng điều khiển, bảng điều khiển đó sẽ trở nên hoạt động (active). Form theo dõi một trường private FActivePdfView: TPdfView. Phản hồi trực quan là một sự thay đổi màu viền trên TScrollBox chứa nó: đặt nó thành clHighlight cho bảng điều khiển hoạt động và clWindow cho các bảng khác. Kết nối điều này với mỗi TPdfView.OnClick và thủ tục mở để focus (tiêu điểm) theo sát tài liệu bạn vừa mở

Một số hoạt động áp dụng cho tất cả các bảng điều khiển hiển thị thay vì chỉ bảng điều khiển đang hoạt động. Một biến boolean FAllViewsMode trên form điều khiển nhánh đó. Khi nó là true, các thay đổi thu phóng và điều hướng trang sẽ được phân tán ra mọi bảng điều khiển có tài liệu đang hoạt động:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Đồng bộ hóa điều hướng trang

Điều hướng được đồng bộ hóa là tùy chọn nhưng hữu ích cho các quy trình duyệt xét tài liệu trong đó cả hai tệp bao phủ cùng một dải trang. Logic nằm trong một trình xử lý sự kiện (event handler) kích hoạt sau khi người dùng điều hướng một dạng xem. Khi một dạng xem nguồn thay đổi PageNumber của nó, trình xử lý sẽ truyền số đó sang các dạng xem khác, chịu một điều kiện bảo vệ: dạng xem đích phải có ít nhất ngần ấy trang, nếu không thì bỏ qua

PageNumber trên TPdfView và trên TPdf độc lập với nhau. TPdf.PageNumber theo dõi trang nào mà thành phần tài liệu coi là hiện tại; TPdfView.PageNumber theo dõi những gì được hiển thị trên màn hình. Cho mục đích điều hướng, bạn muốn thuộc tính của dạng xem (view), không phải thuộc tính của tài liệu (document)

Một hộp kiểm (checkbox) có nhãn đại loại như "Đồng bộ trang" ("Sync pages") mang lại cho người dùng quyền kiểm soát. Khi nó không được đánh dấu, mỗi bảng điều khiển sẽ điều hướng độc lập và trình xử lý thoát ra ngay lập tức. Sự độc lập đó rất quan trọng cho các trường hợp sử dụng trong đó hai tài liệu có số lượng trang khác nhau, hoặc nơi người dùng muốn tìm đoạn văn tương đương trong một bản dịch bắt đầu ở một trang khác. Việc luôn ép buộc đồng bộ hóa sẽ khiến công cụ khó sử dụng hơn một cách sắp xếp hai cửa sổ màn hình đơn giản

Một điều cần lưu ý: việc thiết lập PdfView.PageNumber bằng mã lập trình bên trong trình xử lý đồng bộ sẽ tự nó kích hoạt sự kiện thay đổi trên dạng xem đó. Đề phòng đệ quy vô hạn bằng một cờ boolean mà bạn thiết lập trước phép gán và xóa ngay sau đó. Cờ này dành cho từng form, không phải từng dạng xem, bởi vì cả ba dạng xem chia sẻ cùng một trình xử lý

Thu phóng theo từng bảng điều khiển

Mỗi TPdfView mang thuộc tính Zoom riêng của nó, một biến Double tính bằng phần trăm trong đó Zoom := 100 có nghĩa là kích thước thực tế (100%). Việc thiết lập nó sẽ ghi đè bất kỳ FitMode nào đang hoạt động. Đối với nút vừa-khít-chiều-rộng (fit-to-width) trên bảng điều khiển đang hoạt động, hãy đọc mức thu phóng vừa khít từ PdfView.PageWidthZoom[PdfView.PageNumber] và gán nó. Đối với vừa-khít-trang (fit-to-page), hãy sử dụng PageZoom[PageNumber]. Cả hai đều là các thuộc tính mảng được lập chỉ mục bởi số trang bắt đầu từ 1 (1-based), vì vậy hãy đề phòng số trang bằng không trước khi truy cập chúng

Khi bạn xuất trang hiện tại thành một hình ảnh, hãy đọc độ xoay từ dạng xem nhưng hãy gọi RenderPage trên thành phần TPdf, không phải dạng xem. Hình thức bitmap của TPdf.RenderPage nhận các kích thước pixel rõ ràng cộng với một giá trị TRotation và một tập hợp TRenderOptions. Biến thể hàm trả về một TBitmap do người gọi sở hữu mà bạn tự giải phóng sau khi lưu:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

Hệ số nhân 2x trên chiều rộng và chiều cao cho kết quả đầu ra sắc nét hơn đối với các tài liệu có văn bản nhỏ. Khối try/finally xung quanh việc giải phóng bitmap không phải là tùy chọn; việc hủy TSaveDialog vẫn đi vào khối finally, và bạn muốn bitmap được giải phóng bất kể người dùng đã làm gì

Yêu cầu DLL

PDFium Component bọc (wrap) thư viện pdfium nguyên bản. Một tiến trình máy chủ 32-bit cần pdfium32.dll; một máy chủ 64-bit cần pdfium64.dll. Các biến thể với công cụ JavaScript V8 thêm hậu tố v8 và nặng khoảng 23-27 MB so với các bản dựng tiêu chuẩn 5-6 MB. Đối với một trình xem so sánh vô hiệu hóa việc điền biểu mẫu (Pdf.FormFill := False), bản dựng không có V8 tiêu chuẩn là đủ và giữ cho kích thước bản phân phối nhỏ hơn

Đặt tệp DLL trong cùng thư mục với tệp thực thi, hoặc trong bất kỳ thư mục nào trên hệ thống PATH. Thành phần sẽ tải nó theo yêu cầu khi TPdf đầu tiên được kích hoạt, do đó, một DLL bị thiếu sẽ bộc lộ tại thời điểm đó thay vì lúc khởi động ứng dụng. Nếu bạn cung cấp một trình cài đặt, cách tiếp cận đáng tin cậy nhất là sao chép DLL vào thư mục ứng dụng trong quá trình cài đặt thay vì dựa vào một thư mục hệ thống mà quản trị viên có thể dọn dẹp sau này

Các bản dựng V8 chủ yếu hữu ích khi bạn cần tương tác với các hành động JavaScript của PDF, ví dụ như để kích hoạt các trường tính toán hoặc trình xử lý gửi đi (submit handlers). Một trình xem so sánh thụ động không có lý do gì để chạy JavaScript; việc thiết lập Pdf.FormFill := False trước khi Active := True bỏ qua hoàn toàn môi trường điền biểu mẫu, điều này cũng có nghĩa là không có công cụ JS nào được khởi tạo ngay cả khi bản dựng tiêu chuẩn được sử dụng. Đó là mặc định chính xác cho một trình xem chỉ-đọc (read-only) bất kể biến thể DLL nào bạn phân phối

Để biết thêm chi tiết về PDFium Component và API đầy đủ của nó, hãy truy cập trang sản phẩm Delphi PDFium Component