PDF Library for Delphi 透過 AddModernImageFromFile 及其串流與字串變體,接受 AVIF、HEIF 與 JPEG XL 影像作為輸入,在寫入 PDF 影像物件的過程中保留 alpha、內嵌的 ICC 描述檔與 16 位元色版。格式偵測是靠一次有邊界限制的魔數讀取來完成,解碼則透過一個可替換的後端執行,因此對一個實際上並非這幾種格式的檔案,不會呼叫任何外部程式
這些格式是透過手機進入文件工作流程的。iOS 多年來預設就產生 HEIC,Android 裝置產生 AVIF,而一位在現場拍攝損壞零件照片的技術人員送出的影像,2015 年打造的 PDF 報表產生器根本完全無法開啟。一般的後備路徑(透過平台點陣圖解碼)能穩定得到 8 位元色彩,卻會在過程中失去 alpha 與色彩描述檔
現代影像路徑保留了哪些點陣圖轉換會失去的東西?
三樣東西,每一樣都對應著一種依賴它的工作流程。Alpha 會保留下來,這對疊在頁面內容上的商標與去背產品圖很重要。ICC 描述檔會保留下來,這對任何要印刷或需要色彩比對的東西很重要。16 位元色版會保留下來,這對醫療與科學影像很重要,因為 8 位元量化會摧毀影像拍攝時原本要捕捉的那些漸層細節
把影像透過平台點陣圖傳遞,這三樣東西會在同一步驟中全部流失,而且是無聲無息地流失:產生出來的 PDF 看起來大致正確,直到印刷廠問企業標準紅為什麼不對了,才有人注意到。現代影像呼叫中選項值為 8 的旗標,正是保留 alpha、ICC 與 16 位元色版的組合,也是這些呼叫的預設值
把影像加到頁面上
這個呼叫會回傳一個影像識別碼,接著可以選定並繪製,或一步到位地繪製並釋放:
uses
PDFlibrary, PDFlibModernImage;
var
Lib: TPDFlib;
ImageID: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.SetPageSize('A4');
Lib.NewPage;
// Options = 8 保留 alpha、ICC 與 16 位元色版
ImageID := Lib.AddModernImageFromFile('site-photo.heic', 8);
if ImageID > 0 then
Lib.DrawImageAndRelease(ImageID, 40, 40, 515, 340)
else
Lib.DrawText(40, 40, 'image could not be decoded');
Lib.SaveToFile('inspection-report.pdf');
finally
Lib.Free;
end;
end;
偵測發生在解碼之前,而且刻意設計得很狹窄。函式庫讀取一段有邊界限制的標頭,辨識用來識別 AVIF 與 HEIF 的 ISO 基礎媒體檔案格式品牌標記,也辨識 JPEG XL 的原始與容器兩種簽章形式,接著還原呼叫端的串流位置。未知或偽裝的輸入永遠不會傳到外部編解碼器,這也防止了一個改了副檔名的執行檔被當成圖片交給解碼器
解碼實際上發生在哪裡?
現代影像格式是龐大且複雜的編解碼器,把其中一個塞進 PDF 函式庫本身會是個奇怪的設計選擇。預設後端會在行程內動態載入一個可部署的 MagickWand 模組,並依一套文件化的順序尋找它:你明確設定的檔案或目錄、環境變數、執行檔所在目錄,以及系統搜尋路徑
已經自帶解碼器、或完全不得載入外部模組的應用程式,則可以改為註冊自己的回呼函式。合約很小:讀取輸入串流、把 PNG 寫入輸出串流、遵守要求的方向設定:
function MyDecoder(InStream, OutPNG: TStream;
ImageFormat: TPDFlibModernImageFormat;
ApplyOrientation: Boolean): Boolean;
begin
// 用你自己的編解碼器解碼 InStream,把 PNG 位元組寫進 OutPNG
Result := DecodeWithBundledCodec(InStream, OutPNG,
ImageFormat, ApplyOrientation);
end;
begin
RegisterModernImageDecoderBackend(MyDecoder);
// ... 新增影像 ...
ClearModernImageDecoderBackend; // 回到預設後端
end;
部署方面有一項便利設計與一項刻意的克制。若編解碼器目錄底下含有一個 modules\coders 子目錄,函式庫會在主應用程式尚未自行設定的情況下,補上這種佈局所需的編解碼器環境變數。已有自己一套執行環境部署策略的應用程式,會保留自己的做法
為什麼中間要經過 PNG?
透過記憶體中的 PNG 而非原始像素緩衝來橋接,看起來像是多出來的一步,實際上卻是成本最低的正確做法。PNG 能表達所有必須保留下來的東西:alpha、色彩類型、位元深度與內嵌的 ICC 描述檔,而函式庫已經有一條從 PNG 進到 PDF 影像物件、搭配正確篩選器與色彩空間的成熟且經過充分測試的路徑。重用它,代表現代格式能繼承多年累積下來的正確性成果,而不必另外打造一套平行實作
這座橋接完全在記憶體中進行,因此不會建立任何暫存檔,也不需要在崩潰時做清理。有一個小狀況需要明確處理:某些轉換在改變格式時會遺失 ICC 描述檔。後端因此會在格式切換之前先擷取來源描述檔,以 Flate 壓縮它,建構一個帶有重新計算 CRC 的合法 iCCP 區塊,並移除任何會與其衝突的 sRGB 區塊。在測試中,一份解碼過的 AVIF 保留了 16 位元 RGBA 搭配 16 位元 alpha,從產生的 PDF 中抽出的描述檔,與來源描述檔在 60,960 位元組上逐位元組完全相符
投入正式環境前的實務提醒
在啟動時就確認可用性,而不是等到第一張照片進來才發現問題。ModernImageCodecAvailable 會回報是否有可用的後端,SetModernImageCodecLibrary 則能在你的部署把編解碼器放在非標準位置時,指向一個明確的檔案或目錄:
Lib.SetModernImageCodecLibrary('C:\MyApp\codecs');
if Lib.ModernImageCodecAvailable = 0 then
Log('modern image input unavailable - HEIC and AVIF will be refused');
留意成品的檔案大小。一張帶有內嵌描述檔的 16 位元 RGBA 影像,是個很大的 PDF 影像物件,一份帶有四十張這種影像的報表會很龐大。當文件的目的地是螢幕檢視而非印刷時,內嵌前先降採樣是正確的取捨,一般的檔案大小調整手段說明於 PDF 檔案大小最佳化 一文
最後,請刻意決定色彩政策。保留來源描述檔,對封存與印刷工作是正確做法;轉換到文件通用的色彩空間,則在一組混雜的照片必須看起來一致時才正確,轉換方式說明於 把文件重新著色到另一個色彩空間 一文。若你需要確認檔案裡實際落地的是什麼,文字、影像與字型擷取 一文所述的檢視路徑,能回報一份文件帶有的影像物件
現代影像輸入、色彩管理與影像最佳化,都是同一套適用 Delphi、C++Builder 與 Free Pascal 函式庫的一部分;完整功能清單列於 PDF Library for Delphi 頁面