技術記事

SVGを再利用可能なForm XObjectとしてPDFにインポート

HotPDFはImportSVGFormXObjectにより、SVGファイルをロード済みまたは作成中のPDFに再利用可能なForm XObjectとしてインポートし、生成されたフォームのリソース名を返す。配置は別呼び出しのDrawSVGFormXObjectで行うため、1回のインポートをアートワークのバイトを一切複製せずに、異なるサイズで多数のページにスタンプできる

この分離は見た目以上に重要である。300 DPIでラスタライズしたロゴを400ページに配置すれば、それはビットマップの400個のコピーになる。同じロゴをForm XObjectとして扱えば、1個のオブジェクトが400回参照されるだけであり、どのズーム倍率でも鮮明さを保ち、ビルド時に十分と思われたDPIではなくデバイス解像度で印刷される

1回のSVGインポートが単一のPDF Form XObjectを生成し、DrawSVGFormXObjectがそれを異なるサイズで多数のページに配置する様子を、何百ものラスタービットマップコピーと比較した図
1回のインポートでアートワークは独自のリソースを持つForm XObjectとして一度だけ書き込まれ、各配置は重複バイトを追加しない小さな呼び出しにすぎない

毎回SVGを描画するのではなくForm XObjectを使う理由

Form XObjectはPDF自身が持つ再利用可能なコンテンツの仕組みであり、独自のバウンディングボックスとリソース辞書を持つ自己完結型のコンテンツストリームを、名前で呼び出す。SVGをそこにインポートすると、直接描画にはない3つの性質が得られる。アートワークはファイル内に1つだけ存在する。各配置は小さな行列と呼び出しにすぎない。そして形状はベクターのまま保たれるため、800パーセントにズームしたビューアはピクセルではなく曲線を描画する

注意すべきはリソースである。フォームのフォントやグラフィックステートは、インポート時にたまたまカレントだったページのリソースではなく、フォーム自身の/Resources辞書に存在しなければならない。HotPDFはフォームが必要とするBase 14フォントとExtGStateエントリをフォーム自体に書き込む。というのも、最初のホストページから名前を借用するフォームはそのページでは正しく描画されても、後で配置される他のすべてのページでは未解決のリソース名を生じさせるためである

2つの反転、1つは幾何形状用、1つはグリフ用

SVGの座標系は左上隅から下方向に成長するが、PDF Form XObjectはPDFユーザー空間を使用し、左下隅から上方向に成長する。ルートのviewBox変換は1回のY軸反転を行い、幾何形状が正しい位置に収まるようにする。すでに反転したフレーム内に描かれたグリフは上下逆になるため、テキストにはさらに2回目のローカルな反転が必要になる

SVGとPDFの座標間で発生する2つのY軸反転を示す図:ルートのviewBox変換が幾何形状を補正し、テキストグリフは直立を保つためにさらに2回目のローカルな反転を必要とする
ルートのviewBox変換が1回Y軸を反転させることで幾何形状はPDF空間に正しく収まり、テキストはさらに2回目のローカルな反転を受けなければ、すべてのグリフが上下逆になってしまう

これは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');

    // 1回のインポートで複数配置:アートワークのバイトは1つだけ存在する
    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のリンクターゲットはページに属する注釈であるため、フォーム内部ではリンクは存続できない。したがって、元のSVGにある<a>要素を単純にフォームのコンテンツストリームへコピーすることはできない

HotPDFはこれを、解析時にリンクの矩形をフォーム空間のメタデータとして保持し、各配置時にページレベルのリンク注釈として実体化することで解決する。DrawSVGFormXObjectは渡されたターゲット矩形を通じてそれらの矩形をマッピングするため、異なるサイズで2回配置されたフォームは、正しくサイズ調整された2組のヒット領域を得る。リンク矩形はルートサイズのみを通じてマッピングされ、コンテンツストリームには一切入らないため、描画されるアートワークはリンクの個数にかかわらず同一のままである。インタラクティブ性なしでアートワークだけが欲しい場合、例えば印刷専用ページでは、AddLinks = Falseを渡す

インポーターが拒否するもの、そしてそれが重要な理由

SVGはスクリプト、外部参照、エンティティ展開を伴うXML方言であり、これにより任意のSVGファイルは任意のHTMLファイルと同様に悪意ある入力になり得る。インポートプロファイルは、ドキュメントの外部に及ぶあらゆるものに対してフェイルクローズドである:外部リソース参照、スクリプト、イベントハンドラ属性、DTD、そしてローカルなフラグメント参照でないurl()すべてが対象となる

フェイルクローズドとは、問題のある構文を黙って除去するのではなく、インポート自体を失敗させることを意味する。この違いはバッチパイプラインにとって重要である。なぜなら、黙った除去は誰も気づかない何かが欠落したドキュメントを生み出すのに対し、拒否は隔離して調査できるファイルを生み出すからである。ユーザーからのSVGアップロードを受け入れる場合、これが望ましい挙動であり、信頼できない画像に対するコーデックのプロセス分離で説明されているプロセスレベルの分離と組み合わせて使える

フェイルクローズドなSVGインポートプロファイルを示す図:ローカルなベクター機能は受け入れつつ、スクリプト・外部参照・DTDについては黙って除去するのではなくインポートを失敗させることで拒否する
インポートプロファイルは完全に失敗させることでスクリプト・外部参照・DTDを拒否し、これにより静かに不完全なドキュメントではなく隔離できるファイルが手元に残る

カラーフォントもレンダリングする場合に知っておくべき内部境界が1つある。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]));

// 同じフォームを2つのサイズで配置すると、2組の正しいリンクセットが得られる
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インポートが他のベクター経路の中でどこに位置づけられるか

3つのベクターインポート経路が存在し、互いに置き換え可能ではない。SVGインポートは、デザインツールで作成されたアートワークや、Webパイプラインから届くあらゆるものに適している。EMFおよびWMFベクターインポートで扱われているメタファイルインポートは、Windowsアプリケーションやデバイスコンテキストに描画するチャート作成コンポーネントが生成するコンテンツに適している。ページAPIによる直接描画は、自分のコードが計算するアートワークに適しており、そこには中間フォーマットが一切存在しない

再利用性の議論は、繰り返し使われる要素に関して特にSVGインポートに有利に働く。レターヘッド、ロゴ、透かし、認証マーク、装飾罫線はいずれも多数のページで同一に描画されるものであり、それぞれが1つのフォームオブジェクトと、ページごとの1回の呼び出しで済む。それが問題にならないと決めつける前に、実際のドキュメントでファイルサイズの差を測定してほしい。ページごとにベクターロゴを含む500ページのカタログでは、1つのフォームと500個のインラインコピーとの差は通常数メガバイトになる

SVGインポート、メタファイルインポート、直接ベクター描画は、DelphiおよびC++Builder向けに同じコンテンツストリームライターを共有している。機能一覧の全体はHotPDF Delphi PDFコンポーネントページにある