審閱期間在段落周圍繪製的矩形,不必成為 PDF 內的標記。HotPDF 的 THPDFViewerModel 提供 AddHighlightRegion,這個方法會將每個標示保留為記憶體中的記錄,而不是對已載入文件的變更,因此審閱者可以標記數十頁內容,同時磁碟上的檔案仍與原檔逐位元組相同。將縮放調整至 6400%、把頁面旋轉 90 度、從 Fit Width 切換到 Fit Page 後,同一個矩形仍會落在同一個段落上,因為座標計算會在繪製標記的當下,透過實際的算繪幾何執行
以 PDF 檢視器為核心的審閱工具經常遇到這個問題。修訂標記畫面、對產生的發票進行 QA、內部簽核流程:這些情境都需要讓使用者標示頁面上的某個區域,同時避免每個草稿標記都變成檔案的永久變更,也不必為了在使用者還在判斷標記是否應保留時顯示彩色方框,就引入完整的註解子系統。HotPDF 透過專用的標示圖層解決這個問題,圖層完全位於 在 Delphi 中以 MVC 架構建立自訂 PDF 檢視器 所描述的 Model 端,因此同一份標示清單也能由不需要視窗控制代碼的單元測試驅動
HotPDF 的 AddHighlightRegion 實際儲存哪些內容
AddHighlightRegion 為每個標記精確儲存三項內容:從零開始的頁面索引、PDF 使用者空間座標中的 THPDFRectangle,以及 TColor,全部封裝為 THPDFViewerHighlight 記錄,位於 THPDFViewerModel 內。呼叫 Viewer.HighlightRegion(PageIndex, PageRect, clYellow),或等效的 Model.AddHighlightRegion,會將其中一筆記錄附加到私有陣列並傳回其索引,而呼叫端取得的唯一控制代碼就是這個索引:沒有獨立物件、沒有參照計數介面,也沒有需要釋放的內容。本文中的其他所有能力,包括繪製標記、縮放變更後重新對映標記,以及刪除標記,都是建立在這筆小型記錄之上
每個矩形在接受前都會先正規化並裁切。AddHighlightRegion 會在審閱者由右向左拖曳時交換左右邊界,在向上拖曳時交換上下邊界,接著依據透過 GetLoadedPageBox 取得的頁面 MediaBox 裁切結果。寬度為零、高度為零,或完全位於頁面外的矩形會直接遭到拒絕:方法傳回 -1,清單中不會新增任何內容。這個回傳值並非裝飾:如果呼叫端不檢查它,從外部審閱檔案重建的標示批次,或在頁面替換後使用過時座標重建的標示批次,都可能悄悄遺失項目
標示在縮放或旋轉後如何維持對齊
標示能維持對齊,是因為 HotPDF 將它儲存在 PDF 頁面空間中,並在每次重繪時重新投影到螢幕空間,而不是儲存會在縮放層級變更瞬間過時的螢幕矩形。THPDFViewerModel.PagePointToView 及其反向方法 ViewPointToPage 會分兩個階段執行投影:先處理頁面自身的 /Rotate 項目,再處理檢視器獨立的 ViewRotation,後者不會寫回 PDF,只影響檢視器顯示的內容。滑鼠放開時還原轉換會以相反順序執行相同兩個階段,因此審閱者在旋轉 270 度的頁面上以高倍率繪製的標示,在將檢視畫面重設為符合頁面後,仍能落在完全正確的位置
用於投影的 DPI 與旋轉同樣重要。HotPDF 的檢視器會在每次算繪後,於 FRenderedDPI 中擷取目前螢幕上點陣圖的精確 DPI,而 ImageMouseUp 會將相同值傳入 ViewPointToPage,因此滑鼠座標永遠依照實際繪製時使用的解析度轉換,而不是依據目前縮放屬性重新計算出的解析度。CreatePageSnapshot 及其相關方法會將 DPI 限制在 12 至 2400 的範圍內,但互動式算繪路徑沒有這項上限:標準縮放階梯最高達 6400%,以預設 96 DPI 基準計算,會遠高於 2400 DPI,因此將快照式限制重用於座標對映,會讓縮放範圍頂端的每個標示偏移數個像素。另有兩個較小的預設值完善了互動行為:任一軸上的拖曳距離短於兩個像素會被視為點擊,不產生標示;在至少一個頁面實際完成算繪前,也不能開始標示,因為 FRenderedDPI 的初始值為零
將互動式標示接入審閱畫面
啟用互動式標示只需在 THPDFViewer 控制項本身設定三個屬性:將 InteractionMode 從預設的 vimBrowse 設為 vimHighlight,選擇 HighlightColor(預設為 clYellow),並處理 OnMarqueeSelect 以得知審閱者剛才繪製的內容。其他工作,包括擷取滑鼠、在審閱者拖曳時繪製點狀選取矩形、將放開位置轉回頁面空間,以及呼叫 AddHighlightRegion,都會在事件觸發前於控制項內部完成
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 只會在拖曳確實產生標示時觸發:太小而不算拖曳的點擊會立即清除選取覆蓋層,而完全落在頁面外的拖曳會抵達 AddHighlightRegion,但會如同程式化呼叫一樣在那裡遭到拒絕,因此無論哪種情況事件都會保持靜默。若標示在控制項邊緣似乎停止回應,有一項實作細節值得注意:滑鼠擷取屬於 THPDFViewer 本身(它是 TScrollBox 的衍生類別),而不是顯示頁面點陣圖的內部 TImage,因此審閱者可以拖曳越過算繪頁面的邊緣,仍能乾淨地完成放開操作
從程式碼新增、移除及重新讀取標示
標示完全不必來自滑鼠拖曳。Viewer.HighlightRegion(PageIndex, PageRect, Color) 會導向互動式拖曳在內部呼叫的同一個 Model.AddHighlightRegion,它之所以公開,是為了讓審閱畫面能從既有資料重建標示:從資料庫載入的留言、文字搜尋結果,或從先前工作階段還原的標記。由於座標只是單純的 PDF 使用者空間數值,這條路徑不依賴頁面是否已先完成算繪;互動式拖曳則需要 FRenderedDPI 已經包含實際值
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;
移除單一標示時,陣列式儲存方式就會顯現出來。RemoveHighlightRegion 會刪除一筆記錄,並將後續每筆記錄向前移動一格以填補空缺,這表示任何先前取得的索引(無論來自 OnMarqueeSelect 事件或先前的列舉),只要清單中位於它前面的項目遭到移除,就不再可信。OnHighlightChange 會在每次新增、移除及呼叫 ClearHighlightRegions 時觸發,但不會提供變更內容的資訊,因此安全模式是將它視為訊號,使用 HighlightCount 與 TryGetHighlightRegion 從頭重建審閱面板正在顯示的清單,而不是就地修補快取索引
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;
何時應該改用真正的 Highlight 註解
當標示需要在該次 THPDFViewer 執行個體之外存續時,就應該將標示區域轉為真正的註解。HotPDF 也提供 AddHighlightAnnotation 供新頁面使用,以及 AddLoadedHighlightAnnotation 供已載入的文件使用;雖然名稱幾乎相同,這是完全不同的機制:兩者都會將真正的 ISO 32000-1 §12.5.6.10 文字標記註解(PDF /Subtype /Highlight)寫入頁面的 /Annots 陣列,以 /QuadPoints 標示精確的字形範圍,而且任何相容的 PDF 檢視器在檔案儲存後都能算繪它,不只是 HotPDF 能夠算繪。相同的機制邊界也決定標記是否能透過 XFDF 往返:使用 AddLoadedHighlightAnnotation 建立的註解是一般 PDF 物件,ExportLoadedAnnotationsToXFDF 會擷取它,並以 ISO 19444-1 標記交給 Acrobat 或其他審閱工具,相關內容見 在 Delphi 中匯入與匯出 PDF 註解為 XFDF;透過 AddHighlightRegion 加入的區域則不會出現在該匯出內容中,因為它從未寫入物件圖:它只存在於建立它的 THPDFViewerModel 存續期間。頁面可用的完整標記與幾何註解類型,以及矩形如何放置每一種註解,請參閱 使用 HotPDF Delphi 元件處理 PDF 註解的文章,實務規則很簡單:文件仍在討論時,讓標記保持可丟棄;決策確定後,再將它提交為註解
標示圖層的界線
至於標示圖層本身,它不會試圖模擬半透明螢光筆:RefreshDocument 會在快取的頁面點陣圖上方,以標示自身的顏色將每個區域繪製為兩像素寬的外框矩形,方式與繪製搜尋命中項相同,而不是在底下文字上方混合彩色填滿,因此經典的黃色暈染效果必須在應用程式程式碼中繪製,或延後交由已提升為註解的外觀串流處理。區域一旦存在,就有一項能力值得重用:CreateCurrentPageRegionSnapshot 會使用標示已攜帶的同一個 THPDFRectangle,只將該區域算繪為點陣圖,適合附加小型預覽圖到審閱留言,而不必匯出整頁。審閱版本不必一開始就選定其中一種機制:只要留言討論串仍在進行,就將每個新標記預設為可丟棄的 THPDFViewerHighlight 區域,等審閱者解決後才呼叫 AddLoadedHighlightAnnotation,如此可在最容易反覆修改的來回討論期間保持已載入的 PDF 不變。本文介紹的檢視器控制項是 Delphi 與 C++Builder 標準 HotPDF Component 的一部分,並與上述其他註解及表單 API 同列