技術文章

在 Delphi 中使用 HotPDF 將 JPEG 2000 影像加入 PDF

掃描的醫療切片、航拍測量圖磚、以完整動態範圍封存的電影影格 - - 這些圖片以 JPEG 2000 格式呈現,而且這樣做是有原因的。這種格式保留每個通道 12 或 16 位元,使用小波變換而非 JPEG 所使用的區塊 DCT 進行壓縮,並且能從同一個碼流以無損或有損方式對相同圖片進行編碼。當從這些來源建立的檔案必須成為 PDF 時,影像必須通過 PDF 規格專為此編解碼器保留的濾鏡

HotPDF v2.228.0 為該路徑恢復了可用的 JPEG 2000 解碼引擎。早期的建置版本附帶了返回 nil 的存根函式,因此 API 存在但無法解碼任何內容。目前的引擎靜態綁定 OpenJPEG 2.5.4,並將 JP2 或 J2K 來源轉換為 HotPDF 可以置入頁面的像素

PDF 中的 JPXDecode 濾鏡

ISO 32000-1 在 §7.4.9 中定義了 JPXDecode 濾鏡。PDF 影像 XObject 在串流字典的 /Filter 條目中命名其壓縮方式,而 JPXDecode 是表示串流資料是 JPEG 2000 碼流(而非 /DCTDecode 所承載的基準 JPEG)的值。這個濾鏡使 PDF 能夠保存具有高位元深度的小波壓縮影像資料,它同時允許該編解碼器的無損和有損模式,因為模式是碼流本身的屬性,而非其外部包裝的屬性

最後這一點值得牢記。JPEG 2000 是一個帶有無損特殊情況的單一演算法,而非兩種獨立的格式。可逆的 5/3 小波能精確重建原始樣本;不可逆的 9/7 小波以精確性換取較小的檔案。解碼器在讀取時對兩者採用相同的處理方式,這就是為什麼 HotPDF 只需一條解碼路徑,即可接受 JPXDecode 串流所拋出的任何內容

解碼器對像素的處理

常見情況下,PDF 影像 XObject 期望 DeviceGray 或 DeviceRGB 中每個元件 8 位元。JPEG 2000 通常超過這個限制,其元件模型也比緊湊的光柵更為通用,因此解碼器在資料可作為普通影像使用之前需要完成三項工作

首先,高位元深度元件被重新取樣至 8 位元。12 位元或 16 位元的樣本被縮放到 0 到 255 的範圍,使結果成為普通的 8 位元光柵。有符號的元件首先被移位到無符號範圍。這個細節很重要,因為它本身就是有損的:16 位元灰階掃描一旦成為 8 位元 PDF 影像,就會失去其深度的色調範圍 - - 對於螢幕和印刷輸出來說這是正確的取捨,但對於重新封存則不然

其次,YCbCr(編解碼器稱之為 SYCC)色彩空間被轉換為 RGB。JPEG 2000 為了壓縮效率,常以亮度-色度空間儲存色彩,與基準 JPEG 使用的概念相同,解碼器套用標準逆變換,使頁面接收到真正的 RGB

第三,子取樣的元件透過最近鄰複製進行上取樣。色度通道通常以半解析度儲存,因此解碼器以各元件自身的尺寸和取樣因子讀取每個元件,然後複製樣本以將每個通道提升至完整影像尺寸後再交錯排列。最近鄰取樣保持步驟低成本;它所填充的色度本來就是低頻的,因此視覺代價很小

Delphi 中 JPEG 2000 解碼路徑管線圖:攜帶 12 或 16 位元取樣、SYCC 色彩與次取樣色度的 JP2 或 J2K 來源,先重新取樣到 8 位元、轉成 RGB、色度上取樣,成為交錯式、可直接作為 DeviceGray 或 DeviceRGB XObject 的點陣
無論 JPXDecode 串流裡裝什麼,解碼器最終都落在普通的 8 位元交錯點陣上,適合 DeviceGray 或 DeviceRGB 的 XObject

JP2 box 容器與原始 J2K 碼流

JPEG 2000 檔案有兩種形式,HotPDF 從前幾個位元組(而非檔案副檔名)偵測正在讀取哪種。JP2 檔案是 box 結構的容器:以 12 位元組的簽名 box 00 00 00 0C 6A 50 20 20 開頭,並將碼流與描述色彩空間、解析度和中繼資料的 box 包裝在一起。原始 J2K 碼流完全沒有容器,以 SOC 標記 FF 4F FF 51 開頭。解碼器讀取這些前導位元組,識別簽名,並為每種情況選擇對應的 OpenJPEG 編解碼器

兩種形式都能處理,因為兩者都在實際中出現。需要側邊中繼資料的擷取裝置和封存系統發出 JP2;想要最小可能負載的工具發出裸碼流。格式類型以列舉 TJpeg2000FileType 建模,其成員包含 jtInvalid、jtJP2、jtJ2K 和 jtJPT。JPT 成員命名了 JPIP 串流變體;位元組簽名偵測器解析它能解碼的兩種形式(JP2 和 J2K),並將其他任何形式報告為 jtInvalid,使不支援的輸入乾淨地失敗,而非產生垃圾

HotPDF 圖解:以開頭位元組判別 JPEG 2000 檔案類型——以 00 00 00 0C 6A 50 20 20 簽章開頭的盒裝 JP2 容器,與以 FF 4F FF 51 SOC 標記開頭的原始 J2K 碼流,餵入首位元組檢查,回傳 jtJP2、jtJ2K 或不支援輸入的 jtInvalid
HotPDF 只憑最開頭的位元組辨識容器,把不支援的特徵映射成 jtInvalid,讓解碼乾淨地失敗
uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 or J2K, auto-detected
      if Decoder.GetImageData(Pixels) then
        // Pixels 是 8-bit 交錯資料,寬度為 ColorComponents 個通道,
        // 由上到下按列排列:可直接用於 DeviceGray/DeviceRGB XObject
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

編碼端的無損與有損

解碼器無需被告知模式即可讀取兩種模式。只有當您反向操作並產生 JPEG 2000 檔案時,選擇才成為一個參數 - - HotPDF 也可以透過 TJpeg2000Bitmap 類別做到這一點,這是一個載入和儲存光柵資料為 JP2 的 TBitmap 後代類別。兩個屬性控制輸出。LosslessCompression 是一個布林值,為 True 時選擇可逆小波;CompressionQuality 是一個 TJpeg2000QualityRange,是一個從 1 到 100 的整數,其中 1 是最小且質量最差的,100 是最大且最忠實的。預設值存在於命名常數中:Jpeg2000DefaultLosslessCompression 為 False,Jpeg2000DefaultLossyQuality 為 80

這個決定是內容決定。無損適合主要副本、醫療或法律掃描、任何可能在以後重新編碼且不能累積世代損失的內容。品質 80 的有損適合用於螢幕或印刷的圖片,小波的優雅降質在讀者不會注意到的情況下提供了明顯較小的檔案。有一個 CMYK 注意事項要標記:點陣圖公開了 SetCMYK,將四通道資料標記為 CMYK 而非 RGBA,這對於保持分色完整的印刷管線很重要

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // 解碼現有的 JP2/J2K
    Bmp.LosslessCompression := True;         // reversible 5/3 wavelet
    // 或使用較小的有損檔案:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // 符合預設值
    Bmp.SaveToStream(Output);                // always writes a JP2 file
  finally
    Bmp.Free;
  end;
end;

為何沒有載入時解碼的濾鏡管線

有一個架構事實影響您使用這一切的方式,而且很容易假設相反的情況。HotPDF 沒有通用的載入時解碼影像濾鏡。當您開啟一個已包含 JPXDecode 影像的 PDF 時,引擎不會解碼該串流,它會完全保留 JPEG 2000 位元組原樣,因此頁面複製或檔案合併會將影像原封不動地傳遞過去,一位元組不差。解碼器有一個單一的進入點,它在建立端:基於檔案的 AddImage,通過副檔名分派處理 .jp2、.j2k、.jpt 和 .jpc 來源

這個拆分是正確的設計,而非限制。在載入時解碼嵌入的 JPX 串流,只為了在儲存時重新編碼,將把無損封存影像轉換為有損影像,並讓每次合併都膨脹 - - 而您只是想將圖片從一個 PDF 移動到另一個 PDF。原樣傳遞串流是無損且快速的操作。解碼被推遲到真正需要的唯一時刻:當您從磁碟提供 JPEG 2000 檔案並要求引擎將其光柵化以置入新頁面時。此時檔案必須成為像素,解碼器才執行

兩條路徑圖對比 HotPDF 的載入與建立:載入路徑中,內嵌的 JPXDecode 位元組在頁面複製或合併時原封不動、逐位元組保留;建立路徑中,AddImage 經 OpenJPEG 對 JP2 檔解碼一次,ShowImage 再放置並重新內嵌該點陣
在文件之間搬移既有影像只是原樣複製位元組;只有 AddImage 把 JP2 或 J2K 檔變成像素時才發生解碼工作

登錄支援並置入影像

JPEG 2000 圖片登錄是在 HPDF_REGISTER_JPEG2000_PICTURE 編譯開關後選擇加入的,預設為關閉。原因是真實的衝突,而非謹慎:將 jp2、j2k 和 jpc 檔案格式全域登錄到 TPicture 可能會干擾 ReportBuilder 的 TppDBImage 所依賴的 BLOB 格式偵測。在沒有該整合的情況下定義此開關,檔案格式就會登錄,使 TPicture 能夠識別它們;若不定義,AddImage 副檔名分派仍然可以直接解碼 JPEG 2000 檔案,因為該路徑根本不經過 TPicture

理解這一點後,置入 JPEG 2000 圖片與任何其他 HotPDF 影像的三步呼叫節奏相同。向 AddImage 提供一個 .jp2 路徑和圖片應如何儲存在輸出中的壓縮類型,然後用 ShowImage 在頁面上定位返回的影像索引

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // .jp2 來源會先透過 OpenJPEG backend 解碼,然後
    // 以此處要求的壓縮重新嵌入
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x、y、width、height 的單位是 points;最後的 0 是旋轉角度
    Pdf.CurrentPage.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

您傳給 AddImage 的壓縮方式控制的是解碼後的圖片如何重新儲存,而非它是如何讀取的。從 JPEG 2000 檔案解碼到點陣圖的圖片,可以以 DCTDecode JPEG、Flate 光柵或其他支援的濾鏡重新儲出,以適合您的檔案。無論如何,先從 JP2 或 J2K 解碼,因此同樣的呼叫接受小波壓縮的來源,並以您的其餘管線所期望的任何形式嵌入它

關於影像和字型如何落入生成的輸出,請參閱我們關於帶字型和影像的報表輸出的說明。當您組裝的檔案重用現有 PDF 的內容時,此處描述的直通行為與物件串流和增量更新中的合併和修訂機制配合使用。JPEG 2000 解碼引擎作為 HotPDF Delphi Component 的一部分提供,適用於 Delphi 和 C++Builder,並附有本部落格其他地方介紹的影像、字型和檔案 API