技術文章

在 Delphi 中使用 PDFium 的低視力 PDF 色彩濾鏡

低視力的讀者在預設對比度下無法辨識白紙上的黑色文字,因此他們會要求深色模式。天真的做法是將渲染出的頁面上每個像素都反轉。這功能一週就發布了,但隔天就壞了:掃描的相片看起來像底片,讀者的黃色螢光筆標記變成了難以辨認的藍色污漬,而且還有人問為什麼印出來的是一片全黑。這個功能真的值得建置,也很容易做得半調子,這兩種結果之間的差距在於一個概念:每個色彩決策都屬於渲染管線中的特定點,而反轉是一個在錯誤階段應用的錯誤工具。這裡的程式碼使用了 PDFium Component,這是一個適用於 Delphi、C++Builder 與 Lazarus、基於 PDFium 的檢視器,其渲染 API 將這些階段分別暴露出來

濾鏡是呈現狀態,絕非檔案狀態

這裡的一項規則可以防止最糟糕的那類錯誤:閱讀模式只會改變點陣圖的產生或後處理方式,沒有別的。PDF 的位元組保持不變,每種模式都可以透過重新渲染來復原,而且「儲存」絕不會將過濾後的外觀寫回檔案中。這聽起來很明顯,直到一位法律審查員在作用中的濾鏡下列印了一份合約,並將反轉的版本歸檔。到了那時,「列印是使用檔案自己的外觀還是螢幕上的外觀?」這個問題就需要在您的規格中得到明確的答案,而不是程式碼路徑的意外。將濾鏡設定保留在檢視器狀態中,在渲染時套用它,並讓每條匯出路徑宣告它使用哪種外觀

遵守這條規則可以帶來雙倍的回報。可逆性是免費的,因為切換模式會從未改變的來源重新渲染:不需要維護復原堆疊(undo stack),一連串的模式變更也不會讓頁面劣化。多視窗情境也基於同樣的原因保持一致。同一份檔案的兩個視圖可以執行不同的模式,因為每個視圖都擁有自己的呈現狀態,而檔案物件則保持共用

先渲染,後轉換

支援的模式是渲染後的點陣圖處理:RenderPage 產生頁面光柵(raster),然後透過一個轉換路徑調整它。該元件附帶了三個轉換作為就地點陣圖作業,即 InvertPdfBitmapDuotonePdfBitmapGrayscalePdfBitmap,這讓模式切換成為一個乾淨的兩階段函式:

function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // dark bg, amber text
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal falls through: the document keeps its own colors
end;

從這個設計可以得出兩件事。第一,轉換成本與點陣圖大小成正比,因此這項工作屬於您的渲染結果被快取的任何地方:對快取的點陣圖進行一次過濾,而不是在每次繪製時進行。第二,由於轉換是在完成的光柵上執行,所以它對文字、向量藝術、影像和註解外觀的影響是一樣的。這種均勻性正是單純的反轉在處理相片時出錯的地方。這就是為什麼雙色調轉換更適合作為純文字為主之檔案的預設值的原因,因為它將亮度對應到選定的從暗到亮的色彩漸層,而不是將色調反轉;對於想要的讀者,反轉仍然可作為一種明確的選擇。更清晰的字形邊緣則是另一個獨立的控制桿。reNoSmoothText 渲染選項會在渲染時關閉文字反鋸齒,並在高縮放比例的高對比模式下有很好的搭配效果

互不一致的兩種灰階

渲染選項包括 reGrayscale,這看起來像是略過後處理步驟的一條捷徑。但它並不是相同的作業:

// Engine-level: grayscale applied during rasterization
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Post-process: render in color, convert the finished bitmap
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

引擎層級的選項適用於影像內容的光柵輸出,但無法觸及向量填色或文字色彩,因此一個有著彩色標題的頁面,可能會傳回灰色的相片和固執的藍色標題。在完成的點陣圖上執行 GrayscalePdfBitmap 則會無條件地轉換所有內容。當您希望影像去飽和,同時保留文字色彩作為訊號時,渲染選項仍然有它的用武之地,一些低視力讀者特別偏好這樣。但如果需求是「灰階頁面」,後處理才是能滿足它的版本。無論您選擇哪條路徑,請記住兩種 RenderPage 多載風格。函式形式會傳回一個呼叫者擁有且必須釋放的點陣圖,一旦濾鏡使執行中的渲染點陣圖數量倍增時,這一點就很重要

背景、選取標記與 PageColor 陷阱

並非每一種舒適度的調整都是轉換。對於對眩光敏感的讀者來說,單純地用暖色調取代白色頁面背景通常就足夠了,而且它有一個專屬的屬性。這個屬性帶有一條容易讓人中招的作用域規則:

// Affects the on-screen view only
PdfView.PageColor := $00D9EDF2;  // warm paper tone behind page content

// RenderPage output ignores PageColor; pass the color explicitly
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor 會改變 TPdfView 所顯示的內容,但透過 RenderPage 產生的點陣圖會保持預設的白色,除非 Color 參數另有指示。其症狀是肯定會發生的:螢幕上顯示著著色的頁面,但使用者匯出或列印時,輸出卻會還原為白色。請將這歸類到第一節提到的相同匯出政策決策中

剩下的色彩屬性定義了覆疊標記:搜尋命中的 HighlightColor、使用者選取文字的 SelectionColor、語音讀字游標的 ReadingWordColor。在您提供的每一個濾鏡下,這每一項都必須重新檢查。一個在白色上有效的琥珀色閱讀游標,在反轉後就會消失;淡藍色的選取區會消失在高對比的背景中。維護每種模式的覆疊調色盤,而不是一組全域的設定,並刻意測試各種組合。濾鏡加上文字轉語音對使用此功能的讀者來說是正常配置,而不是極端情況。覆疊機制本身已在無障礙閱讀器文章中探討

數字、驗證與列印問題

WCAG 2.1 將這項功能變成了您可以衡量的東西。成功準則 1.4.3 要求正文有 4.5:1 的對比度,而 1.4.6 則為增強對比度將其提高到 7:1。使用對比度分析儀對實際的渲染輸出進行測試,針對這些比例抽查您的高對比模式。文字在影像之上以及表單欄位中的文字,是即使正文通過,這些比例也會默默失敗的地方

列印值得自己的一個決策,站得住腳的預設值是檔案自己的外觀,並將「如顯示般列印」作為一個明確的使用者選項提供。在比檢視器作者預期的更多工作流程中,列印出來的頁面是證據,而一份反轉列印出來的合約,會是一起帶有法律色彩的支援事件。為求效能,還有一點搭配也很重要:過濾的渲染會在每次模式切換時使點陣圖的工作量倍增,所以不要在每次的 paint 訊息上套用轉換。快取過濾後的點陣圖,只有在頁面、縮放比例或模式真正改變時才重新執行轉換。讓這項操作變便宜的快取策略,就在渲染快取與縮放效能文章

有一件事要在您的 UI 中解決而不是在程式碼中:哪種模式是正確的預設值。這沒有單一的答案,所以提供一組選項,讓讀者自行挑選。高對比度適合大多數文字繁重的閱讀,反轉適合特別想要白底黑字的讀者,灰階可以減少色彩雜訊,而背景色調可以處理對眩光敏感的問題。為每個使用者保留他們的選擇,在啟動時恢復它,並保留一個單鍵回到正常狀態的路徑,因為讀者如果陷入一個他們無法閱讀的模式中,會需要一個快速脫身的辦法

這裡使用的渲染選項、點陣圖轉換與視圖色彩屬性,都隨附於 Delphi、C++Builder 與 Lazarus/FPC 適用的 PDFium Component中,並提供完整原始碼,以便對轉換實作進行稽核或擴充