Một hình chữ nhật vẽ quanh một đoạn văn trong lúc review không nhất thiết phải trở thành một dấu vết bên trong PDF. THPDFViewerModel của HotPDF phơi bày AddHighlightRegion, một phương thức giữ mỗi highlight như một bản ghi trong bộ nhớ thay vì một thay đổi vào tài liệu đã tải, nên một người review có thể đánh dấu hàng chục trang trong khi file trên đĩa vẫn giữ nguyên từng byte như ban đầu. Zoom lên 6400%, xoay trang 90 độ, chuyển từ Fit Width sang Fit Page, cùng một hình chữ nhật vẫn nằm đúng trên cùng một đoạn văn, vì phép toán tọa độ chạy qua chính hình học render thực tế tại thời điểm dấu đánh dấu được vẽ
Các công cụ review được xây dựng quanh một trình xem PDF liên tục gặp phải vấn đề này. Một màn hình redlining, một lượt QA trên các hóa đơn được sinh tự động, một quy trình ký duyệt nội bộ: tất cả đều cần cho phép ai đó thu hút sự chú ý vào một vùng của trang mà không khiến mỗi dấu nháp trở thành một thay đổi vĩnh viễn vào file, và không cần với tới cả một hệ thống chú thích đầy đủ chỉ để hiển thị một hộp màu trong khi ai đó vẫn đang quyết định liệu dấu đó có nên giữ lại hay không. HotPDF trả lời điều đó bằng một lớp highlight riêng nằm hoàn toàn ở phía Model của sự tách bạch được mô tả trong xây dựng trình xem PDF tùy chỉnh với kiến trúc MVC trong Delphi, và đó cũng là lý do vì sao cùng danh sách highlight có thể được điều khiển từ một unit test mà không cần bóng dáng một window handle nào
AddHighlightRegion của HotPDF thực sự lưu trữ những gì?
AddHighlightRegion lưu trữ đúng ba thứ cho mỗi dấu đánh dấu: một chỉ số trang bắt đầu từ 0, một THPDFRectangle theo tọa độ không gian người dùng PDF, và một TColor, tất cả được đóng gói thành một bản ghi THPDFViewerHighlight bên trong THPDFViewerModel. Gọi Viewer.HighlightRegion(PageIndex, PageRect, clYellow), hoặc Model.AddHighlightRegion tương đương, sẽ nối thêm một trong các bản ghi này vào một mảng private và trả về chỉ số của nó, và chỉ số đó là handle duy nhất mà caller nhận lại: không có đối tượng riêng biệt nào, không có interface đếm tham chiếu, không có gì cần giải phóng. Mọi khả năng khác trong bài viết này, vẽ dấu, ánh xạ lại nó sau khi zoom thay đổi, xóa nó, đều được xây dựng trên nền của một bản ghi nhỏ đó
Mỗi hình chữ nhật đều được chuẩn hóa và cắt xén trước khi được chấp nhận. AddHighlightRegion hoán đổi cạnh trái và phải nếu người review kéo từ phải sang trái, hoán đổi trên và dưới cho một thao tác kéo hướng lên, sau đó cắt kết quả theo MediaBox của trang lấy được qua GetLoadedPageBox. Một hình chữ nhật kết thúc với chiều rộng bằng không, chiều cao bằng không, hoặc nằm hoàn toàn ngoài trang sẽ bị từ chối thẳng thừng: phương thức trả về -1 và không có gì được thêm vào danh sách. Giá trị trả về đó không phải trang trí: một lô highlight được dựng lại từ một file review bên ngoài, hoặc từ tọa độ cũ sau khi một trang bị thay thế, có thể âm thầm mất bớt các mục nếu caller không kiểm tra giá trị này
Một highlight giữ được sự thẳng hàng sau khi zoom hoặc xoay như thế nào?
Một highlight giữ được sự thẳng hàng vì HotPDF lưu trữ nó trong không gian trang PDF và chiếu lại nó vào không gian màn hình ở mỗi lần vẽ lại, thay vì lưu trữ một hình chữ nhật màn hình vốn sẽ lỗi thời ngay khi mức zoom thay đổi. THPDFViewerModel.PagePointToView và nghịch đảo của nó, ViewPointToPage, thực hiện phép chiếu đó theo hai giai đoạn: trước tiên là entry /Rotate riêng của trang, sau đó là ViewRotation độc lập của Viewer, thứ không bao giờ được ghi ngược lại vào PDF và chỉ ảnh hưởng đến những gì Viewer hiển thị. Việc hoàn tác phép biến đổi khi thả chuột chạy cùng hai giai đoạn đó theo thứ tự ngược lại, và đó là điều cho phép một highlight được vẽ ở mức zoom cao trên một trang đã xoay 270 độ vẫn nằm đúng vị trí sau khi người review đặt lại view về Fit Page
DPI dùng cho phép chiếu đó quan trọng không kém gì phép xoay. Viewer của HotPDF ghi lại DPI chính xác của bitmap đang hiển thị trên màn hình vào FRenderedDPI ngay sau mỗi lần render, và ImageMouseUp truyền chính giá trị đó vào ViewPointToPage để một tọa độ chuột luôn được chuyển đổi bằng đúng độ phân giải nó thực sự được vẽ ra, chứ không phải một độ phân giải tính lại từ thuộc tính zoom hiện tại. CreatePageSnapshot và các hàm liên quan giới hạn DPI trong khoảng từ 12 đến 2400, nhưng đường render tương tác không mang trần giới hạn như vậy: thang zoom chuẩn đạt đỉnh ở 6400%, tương đương hơn 2400 DPI tính theo mức nền mặc định 96 DPI, nên tái sử dụng giới hạn kiểu snapshot cho việc ánh xạ tọa độ sẽ làm lệch mỗi highlight vài pixel ở đỉnh dải zoom. Hai mặc định nhỏ hơn hoàn thiện phần tương tác: một thao tác kéo ngắn hơn hai pixel trên cả hai trục được coi là một cú nhấp chuột và không tạo ra highlight nào, và việc highlight không thể bắt đầu cho đến khi ít nhất một trang đã thực sự được render, vì FRenderedDPI khởi đầu bằng không
Kết nối tính năng highlight tương tác vào một màn hình review
Bật tính năng highlight tương tác là việc của ba thuộc tính trên chính điều khiển THPDFViewer: đặt InteractionMode thành vimHighlight thay vì vimBrowse mặc định, chọn một HighlightColor, mặc định là clYellow, và xử lý OnMarqueeSelect để biết người review vừa vẽ gì. Mọi thứ khác, bắt chuột, vẽ hình chữ nhật chọn kiểu chấm khi người review kéo, chuyển đổi điểm thả về không gian trang, gọi AddHighlightRegion, đều diễn ra bên trong điều khiển trước khi sự kiện đó phát ra
type
TReviewForm = class(TForm)
Viewer: THPDFViewer;
ReviewLog: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle;
HighlightIndex: Integer);
end;
// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
Viewer.PDFDocument := PdfDoc;
Viewer.InteractionMode := vimHighlight;
Viewer.HighlightColor := clLime;
Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;
procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
[PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
PageRect.Right, PageRect.Top]));
end;
OnMarqueeSelect chỉ phát ra cho một thao tác kéo thực sự tạo ra một highlight: một cú nhấp quá nhỏ để tính là một thao tác kéo sẽ xóa lớp phủ chọn ngay lập tức, và một thao tác kéo nằm hoàn toàn ngoài trang vẫn đến được AddHighlightRegion nhưng bị từ chối ở đó theo cùng cách một lệnh gọi bằng code sẽ bị từ chối, nên sự kiện vẫn im lặng trong cả hai trường hợp. Một chi tiết triển khai đáng biết nếu tính năng highlight có vẻ ngừng phản hồi ở các cạnh của điều khiển: việc bắt chuột thuộc về chính THPDFViewer, một hậu duệ của TScrollBox, không thuộc về TImage nội bộ hiển thị bitmap của trang, và đó là điều cho phép người review kéo vượt qua cạnh của trang đã render mà vẫn nhận được một lần thả chuột sạch sẽ
Thêm, xóa, và đọc lại highlight từ code
Highlight hoàn toàn không nhất thiết phải đến từ một thao tác kéo chuột. Viewer.HighlightRegion(PageIndex, PageRect, Color), hàm dẫn vào cùng Model.AddHighlightRegion mà thao tác kéo tương tác gọi nội bộ, là public chính vì lý do đó: để một màn hình review có thể dựng lại các highlight từ dữ liệu nó đã có sẵn: bình luận nạp từ cơ sở dữ liệu, kết quả từ một lượt tìm kiếm văn bản, hoặc các dấu được khôi phục từ một phiên trước. Vì tọa độ chỉ là các con số không gian người dùng PDF thuần túy, không có gì trên đường xử lý này phụ thuộc vào việc một trang đã được render trước đó, khác với thao tác kéo tương tác, vốn cần FRenderedDPI đã mang một giá trị thực
var
I: Integer;
Item: TPriorComment; // your own record: PageIndex + PageRect
NewIndex: Integer;
begin
for I := 0 to PriorComments.Count - 1 do
begin
Item := TPriorComment(PriorComments[I]);
NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
if NewIndex < 0 then
LogWarning('comment %d fell outside the page and was dropped', [I]);
end;
end;
Việc xóa một highlight duy nhất là nơi cách lưu trữ dựa trên mảng lộ rõ ra. RemoveHighlightRegion xóa một bản ghi và dịch mọi bản ghi phía sau xuống một vị trí để lấp khoảng trống, nghĩa là bất kỳ chỉ số nào được ghi lại trước đó, từ một sự kiện OnMarqueeSelect hay từ một lượt liệt kê trước, không còn đáng tin cậy nữa một khi có thứ gì đó phía trước nó trong danh sách bị xóa. OnHighlightChange phát ra ở mỗi lần thêm, xóa, và lệnh gọi ClearHighlightRegions, nhưng nó không mang theo thông tin gì về việc đã thay đổi, nên khuôn mẫu an toàn là coi nó như một tín hiệu để dựng lại bất kỳ danh sách nào một bảng review đang hiển thị từ HighlightCount và TryGetHighlightRegion, thay vì vá một chỉ số đã cache tại chỗ
procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
I: Integer;
Mark: THPDFViewerHighlight;
begin
MarkList.Items.Clear;
for I := 0 to Viewer.Model.HighlightCount - 1 do
if Viewer.Model.TryGetHighlightRegion(I, Mark) then
MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
TObject(I));
end;
Khi nào một dấu đánh dấu nên trở thành một chú thích Highlight thật sự?
Một vùng highlight nên trở thành một chú thích thật sự ngay khi nó cần tồn tại lâu dài bên ngoài riêng instance THPDFViewer đó. HotPDF cũng phơi bày AddHighlightAnnotation cho một trang mới và AddLoadedHighlightAnnotation cho một tài liệu đã tải sẵn, và mặc dù tên gần như giống hệt, đây là một cơ chế hoàn toàn khác: cả hai đều ghi một chú thích đánh dấu văn bản (text-markup) thật sự theo ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, vào mảng /Annots của trang, với /QuadPoints đánh dấu chính xác chuỗi glyph, và bất kỳ trình xem PDF tuân thủ chuẩn nào cũng sẽ render nó một khi file được lưu, không chỉ riêng HotPDF. Cùng ranh giới cơ chế đó quyết định liệu một dấu có khứ hồi được qua XFDF hay không: một chú thích tạo bằng AddLoadedHighlightAnnotation là một đối tượng PDF bình thường mà ExportLoadedAnnotationsToXFDF nhặt lấy và giao cho Acrobat hay một công cụ review khác dưới dạng markup ISO 19444-1, được nói đến trong nhập và xuất chú thích PDF dưới dạng XFDF trong Delphi, trong khi một vùng thêm qua AddHighlightRegion vô hình với lượt xuất đó vì nó chưa bao giờ được ghi vào đồ thị đối tượng: nó chỉ tồn tại chừng nào THPDFViewerModel đã tạo ra nó còn tồn tại. Toàn bộ họ các loại chú thích markup và hình học có sẵn trên một trang, và cách một hình chữ nhật đặt từng loại, được nói đến trong bài viết về chú thích PDF trong Delphi với HotPDF, và quy tắc thực tế rất đơn giản: giữ một dấu ở dạng có thể vứt bỏ trong khi tài liệu vẫn còn đang được thảo luận, và cam kết nó thành một chú thích một khi quyết định đã chốt
Giới hạn của lớp highlight
Về phần mình, lớp highlight không cố gắng trông giống một cây bút highlight trong suốt: RefreshDocument vẽ mỗi vùng như một hình chữ nhật viền hai pixel theo màu riêng của nó chồng lên bitmap trang đã cache, cùng cách nó vẽ kết quả tìm kiếm, thay vì pha trộn một lớp fill màu lên trên văn bản bên dưới, nên một hiệu ứng phủ vàng cổ điển phải được vẽ trong code ứng dụng hoặc để dành cho appearance stream riêng của một chú thích đã được nâng cấp. Một khả năng đáng tận dụng lại một khi một vùng đã tồn tại là CreateCurrentPageRegionSnapshot, hàm nhận cùng THPDFRectangle mà một highlight đã mang theo và render đúng vùng đó thành một bitmap, hữu ích để đính kèm một ảnh xem trước nhỏ vào một bình luận review mà không cần xuất toàn bộ trang. Một bản build review không cần chọn giữa hai cơ chế ngay từ đầu: mặc định mỗi dấu mới là một vùng THPDFViewerHighlight có thể vứt bỏ trong suốt thời gian một luồng bình luận còn mở, và chỉ gọi AddLoadedHighlightAnnotation một khi người review đã chốt nó, điều này giữ cho PDF đã tải không bị đụng chạm trong suốt quá trình qua lại vốn tạo ra nhiều thay đổi nhất. Điều khiển trình xem được mô tả ở đây là một phần của HotPDF Component tiêu chuẩn dành cho Delphi và C++Builder, cùng với phần còn lại của các API chú thích và biểu mẫu được nhắc đến ở trên