技術文章

將 SVG 匯入 PDF 作為可重複使用的 Form XObject

HotPDF 透過 ImportSVGFormXObject 將 SVG 檔案匯入已載入或新建的 PDF,成為可重複使用的 Form XObject,該方法會傳回新建表單物件的資源名稱。放置動作則是另一個獨立呼叫 DrawSVGFormXObject,因此一次匯入即可在多個頁面以不同尺寸蓋印,不必重複任何一個位元組的圖稿資料

這個拆分的意義比聽起來重要。將標誌以 300 DPI 點陣化後貼到 400 個頁面上,就是 400 份點陣圖副本。同一個標誌若做成 Form XObject,則是一個物件被參照 400 次,在任何縮放比例下都維持清晰,並以裝置解析度輸出,而不是建置時「看起來夠用」的某個 DPI

示意圖:一次 SVG 匯入建立單一 PDF Form XObject,DrawSVGFormXObject 再以不同尺寸將其放置到多個頁面,對照數百份點陣圖副本
一次匯入即把圖稿連同其自身資源寫入為一個 Form XObject,之後每次放置都只是一次輕量呼叫,不會新增任何重複位元組

為何用 Form XObject,而非每次都重繪 SVG?

Form XObject 是 PDF 本身用來重複使用內容的機制:一段擁有自身邊界框與資源字典、以名稱呼叫的獨立內容串流。將 SVG 匯入其中,可獲得直接繪製所沒有的三項特性。圖稿在檔案中只存在一份;每次放置只是一個小型矩陣加上一次呼叫;而且圖形維持向量狀態,因此檢視器在 800% 縮放下繪製的是曲線,而非像素

細節在於資源。表單的字型與圖形狀態必須存放在表單自身的 /Resources 字典中,而不是匯入當下剛好作用中的頁面資源。HotPDF 會把表單所需的 Base 14 字型與 ExtGState 項目直接寫入表單本身,因為若表單借用了初次放置所在頁面的名稱,雖然在該頁能正確渲染,日後放到其他頁面時就會產生無法解析的資源名稱

兩次翻轉:一次處理幾何,一次處理字形

SVG 的座標系統從左上角開始向下增長;PDF Form XObject 使用的 PDF 使用者空間則是從左下角向上增長。根層級的 viewBox 轉換會執行一次 Y 軸翻轉,讓幾何圖形落在正確位置。文字接著需要第二次局部翻轉,因為若字形是在已翻轉過的畫面中繪製,會呈現上下顛倒

示意圖:SVG 與 PDF 座標之間的兩次 Y 軸翻轉,根層級 viewBox 轉換修正幾何圖形,文字字形則需要第二次局部翻轉才能保持正立
根層級 viewBox 轉換先翻轉一次 Y 軸,讓幾何圖形落入 PDF 空間,文字還需再做一次局部翻轉,否則每個字形都會上下顛倒

這與 PDF 頁面匯出為 SVG 時所出現的反向修正相同,詳見將 PDF 頁面匯出為 SVG一文。做錯這一步在含文字的圖稿中很容易看出來,在不含文字的圖稿中卻幾乎看不出來,因此帶標語的標誌比單純的標誌更適合作為驗證用例

uses
  HPDFDoc;

var
  Pdf: THotPDF;
  SvgBytes: TBytes;
  FormName: AnsiString;
  Stats: THPDFSVGImportStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalogue.pdf';
    Pdf.BeginDoc;

    SvgBytes := LoadFileBytes('brand-logo.svg');
    FormName := Pdf.ImportSVGFormXObject(SvgBytes, Stats);
    if FormName = '' then
      raise Exception.Create('SVG rejected - see the import statistics');

    // 一次匯入,多次放置:圖稿位元組只存在一份
    for I := 1 to 40 do
    begin
      if I > 1 then
        Pdf.AddPage;
      Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 780, 120, 40);
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

超連結如何在過程中保留?

它們無法保留在表單內部,因為 Form XObject 屬於內容,而 PDF 連結目標是附註(annotation),附註屬於頁面。因此來源 SVG 中的 <a> 元素不能直接複製到表單的內容串流中

HotPDF 的解法是在剖析階段將連結矩形保留為表單空間中的中繼資料,並在每次放置時具體化為頁面層級的連結附註。DrawSVGFormXObject 會依你傳入的目標矩形對這些矩形進行映射,因此同一個表單以不同尺寸放置兩次,會得到兩組尺寸正確的可點擊區域。連結矩形僅透過根尺寸進行映射,絕不會進入內容串流,這讓繪製出的圖稿無論帶有多少連結都維持一致。若想要圖稿但不需要互動性,例如僅供列印的頁面,可傳入 AddLinks = False

匯入器拒絕哪些內容,這正是重點所在

SVG 是一種帶有指令碼、外部參照與實體展開能力的 XML 方言,這使得任意一個 SVG 檔案,與任意一個 HTML 檔案一樣,都可能是具敵意的輸入來源。此匯入設定檔對任何觸及文件外部的內容一律採「失敗即拒絕」(fail-closed)策略:外部資源參照、指令碼、事件處理常式屬性、DTD,以及任何非本機片段參照的 url()

失敗即拒絕代表匯入會直接失敗,而不是悄悄捨棄有問題的結構。這個差異對批次處理流程很重要,因為悄悄捨棄會產生一份缺了什麼卻沒人知道的文件,而拒絕則會產生一個你可以隔離檢查的檔案。若你接受使用者上傳的 SVG,這正是你需要的行為,並可搭配針對不受信任影像的編解碼器行程隔離一併使用

示意圖:失敗即拒絕的 SVG 匯入設定檔接受本機向量特性,並以直接讓匯入失敗、而非悄悄捨棄的方式拒絕指令碼、外部參照與 DTD
此匯入設定檔以直接失敗來拒絕指令碼、外部參照與 DTD,讓你得到一個可隔離檢查的檔案,而不是一份悄悄缺漏內容的文件

若你也處理彩色字型渲染,有一個內部邊界值得了解。用於 OpenType SVG 彩色字形的嚴格 SVG 子集透過另一個獨立進入點執行,並維持其原本較嚴格的失敗行為。一般圖稿匯入是另一條獨立路徑,因此為匯入圖稿新增弧線、透明度與 CSS 支援,並未放寬字型的字形所能包含的範圍

漸層、遮罩與零點陣化原則

漸層會匯入為受原始幾何圖形裁剪路徑限制的有界向量色帶,而非點陣化影像。遮罩採用相同做法:遮罩輪廓會轉換為向量裁剪路徑。結果是漸層豐富的插圖能維持解析度無關且檔案體積小,而點陣化匯入器則會產生一個在匯入當下就固定畫質的大型影像物件

這個做法的代價在於極端情況下的保真度。以色帶近似平滑漸層,在數學上並不等同於原始 SVG 漸層,而含有數十個重疊柔邊遮罩的圖稿,若改以高解析度影像渲染,看起來會更接近原始效果。關於 PDF 原生支援的漸層模型,以及何時該直接使用它們,請參見繪製軸向與放射狀漸層

// 讀取匯入統計資訊可得知實際匯入了哪些內容
Writeln(Format('nodes=%d css rules=%d gradients=%d masks=%d',
  [Stats.NodeCount, Stats.CSSRuleCount, Stats.GradientCount,
   Stats.MaskCount]));
Writeln(Format('text runs=%d links=%d form bytes=%d',
  [Stats.TextRunCount, Stats.LinkCount, Stats.OutputBytes]));

// 以兩種尺寸放置同一個表單,會產生兩組正確的連結集合
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 700, 240, 80);
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 320, 700, 120, 40);

// 僅供列印的放置:圖稿不含可點擊區域
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 60, 120, 40, False);

SVG 匯入在向量匯入路徑中的定位

三種向量匯入路徑並存,彼此並非可互換。SVG 匯入適合設計工具產出的圖稿,以及任何來自網頁流程的內容。中繼檔匯入,涵蓋於EMF 與 WMF 向量匯入,適合 Windows 應用程式與繪製至裝置內容(device context)的圖表元件所產生的內容。透過頁面 API 直接繪製,則適合你自己程式碼運算出的圖稿,這種情況完全沒有中介格式

可重複使用性這項優勢,特別有利於重複出現的元件採用 SVG 匯入。信頭、標誌、浮水印、認證標記與裝飾線條,都是在許多頁面上完全相同地繪製,而其中每一個都只需要一個表單物件加上每頁一次呼叫。在下結論說差異不重要之前,先在實際文件上測量檔案大小差異:在一份 500 頁、每頁都有向量標誌的型錄中,一個表單與 500 份內嵌副本之間的差距,通常達數 MB

SVG 匯入、中繼檔匯入與直接向量繪製,在 Delphi 與 C++Builder 中共用同一套內容串流寫入器;完整功能清單請見HotPDF Delphi PDF 元件頁面