DelphiおよびC++Builder向けネイティブVCL PDFコンポーネントであるHotPDFは、サンプルグリッドではなく数式から構築される3種類のPDF関数タイプを評価する:Type 2の指数補間、Type 3のステッチング、そしてType 4のPostScript計算関数であり、それぞれISO 32000-1 §7.10.3、§7.10.4、§7.10.5に対応する。Type 2は2つの出力ベクトルの間を曲線に沿ってブレンドし、Type 3は1つの入力ドメインにまたがって複数のサブ関数を連結し、Type 4は分岐・比較・そしてコンテンツストリームがその入力から必要とするほぼあらゆることを計算できる制限付きPostScriptプログラムを実行する。この3つのいずれかを少しでも間違えると、その失敗はバグとして名乗り出ることは決してない——それは完全に平坦な帯のあるグラデーション、純粋な黒としてレンダリングされるスポットカラー、あるいはテストスイートがたまたま試さなかった入力でちょうど1だけずれた計算関数として現れる
この3つは4番目のType 0と並んでおり、Type 0は数式ではなくサンプリングされたグリッドを保存する。これはType 0カラールックアップテーブルの関連記事で別途扱われている。この2つのファミリーは同じ問題、すなわち入力を出力にマッピングするという問題を解くが、Type 0は一度計算されファイルに焼き込まれたデータであるのに対し、Type 2、3、4はリーダーが呼び出しのたびに評価するコードである。この4つはすべて、関数辞書の/FunctionTypeエントリで分岐するHotPDFのレンダラー内の一つのディスパッチポイントを共有しているため、シェーディング、色調変換、あるいはハーフトーンのスポット関数は、色を要求する前に4つのうちどれを受け取ったかを知る必要が一切ない
PDF Type 2指数関数はどのように動作するのか
PDF Type 2関数は一つの式を計算する——y = C0 + x^N × (C1 − C0)、これは成分ごとに適用される——ここでxはこの関数の唯一の入力であり、式が実行される前にその/Domainに対して正規化される(ISO 32000-1 §7.10.3)。/C0と/C1はその範囲の両端における出力ベクトルであり、出力成分ごとに1つの数値を持つ。/Nはその間の曲線を形作る指数である:N = 1は多くのグラデーションストップやデュオトーン変換の背後にある直線的なランプを与え、N が1より大きいと曲線はC0側に引き寄せられ、Nが0から1の間だとC1側に押しやられる。RegisterExponentialFunctionは5つの引数からその辞書を構築し、シェーディング、ハーフトーンのスポット関数、あるいは仕様が/Functionキーを受け付けるその他あらゆる場所に組み込める準備の整った関数オブジェクトを返す
C0とC1の間の成分数の関係は2回重要になる:Type 2関数を作成するときに一度、そしてHotPDFが自分で作成していないものをレンダリングしなければならないときにもう一度である。作成側ではRegisterExponentialFunctionがC0とC1を互いに照合し、食い違っていれば例外を発生させる。そのためBeginDocに到達する呼び出しはすでに自己整合的な関数オブジェクトになっている。しかしレンダリング側では、評価器はソースファイルが実際に宣言している/C0と/C1の配列が何であれそれを信頼しなければならない——たとえばプレビュー用に開かれた印刷会社のファイルや、ユーザーに表示し直された署名済み文書である——そして2.376.0より前のバージョンは、これらの配列を4成分(CMYKの場合)用にサイズ設定されたバッファに読み込んでいた。1要素または3要素の/C0と/C1を持つDeviceGrayまたはDeviceRGBの指数トーンは、その読み込みに静かに失敗し、両方の配列がゼロのままになった。そのため、その色調は意図した色ではなく平坦な黒として塗られていた。バージョン2.376.0は、リーダーを固定バッファではなく関数が実際に宣言している出力数にリサイズした——これはまさに、非CMYKのテストケースだけが露呈させる類のバグである。既存のテストスイートは一貫してCMYKで実行されていたため、4を4に入れることは常にぴったり収まっていたからだ
var
EaseIn: THPDFDictionaryObject;
begin
// Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
EaseIn := Pdf.RegisterExponentialFunction(
[0,1], // Domain: single input, clamped to [0,1]
[0, 0, 0], // C0: output at x = 0
[0.8, 0, 0], // C1: output at x = 1
3, // N: exponent, 1 = linear, > 1 eases toward C0
[]); // Range omitted: defaults to a [0,1] clamp per output
end;
Type 3ステッチング:Bounds配列にまたがってサブ関数を連結する
PDF Type 3関数は、単一入力の/Domainにわたる一つの区分的マッピングにk個のサブ関数をステッチ(連結)し、それを機能させる2つの配列が/Boundsと/Encodeである(ISO 32000-1 §7.10.4)。/Boundsは/Domainをk個の連続する区間に切り分けるk − 1個の内部分割点を保持する。評価器は入力を超える上限を持つ最初の区間を選ぶか、入力が最終的な境界に達していれば最後の区間を選び、その区間のサブ関数に処理を委ねる。次に/Encodeは、入力をその区間内の位置から選ばれたサブ関数自身が期待する入力範囲——サブ関数がもう一つの指数セグメントであれば通常は[0, 1]——へ再マッピングし、その後評価が一段深く、そのサブ関数自身の/Domainと/Rangeへと続く
HotPDFのステッチング評価器はかつて正確に2つのサブ関数しか扱えず、その/Boundsリーダーは完全な8要素の配列を要求していた。そのため、2セグメントのグラデーションが実際に必要とする単一の分割点——/Bounds中の1つの数値——は常に解析に失敗し、関数は何も返さなかった。/Encodeは一切適用されていなかった。バージョン2.376.0は、選択処理を仕様が説明する一般的なk個のサブ関数の探索として書き直し、/Boundsをその実際に宣言された長さに対して読み取るようにした。そのため、それだけの数の指数セグメントから連結された3点、4点、あるいは5点ストップのグラデーションが、2セグメントのものが常に主張していたのと同じように解決されるようになった。以下の例は、2セグメントの黒から赤、赤から白へのランプを構築する。これは、軸方向またはラジアルシェーディングが、単一の指数曲線ではデザインが求めるすべてのカラーストップを運びきれないときに必要とする形である
var
ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
// Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
ToRed := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0], [0.8, 0, 0], 1, []);
ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1], 1, []);
Ramp := Pdf.RegisterStitchingFunction(
[0,1], // Domain: the stitched function's own input range
[ToRed, ToWhite], // Functions: k = 2 sub-functions
[0.5], // Bounds: k - 1 = 1 split point
[0,1, 0,1], // Encode: 2 numbers per sub-function
[]); // Range omitted: inherited from each sub-function
end;
Type 4のPostScript計算関数はType 2やType 3にできないことをどう実現できるのか
PDF Type 4関数は、意図的に制限されているとはいえ本物のプログラムを実行する:それはPostScriptの計算機であり、入力をオペランドスタックに積み、算術・比較・スタック操作・ブール演算子に加えてif/ifelse条件分岐を実行し、終了時に出力をスタックに残す(ISO 32000-1 §7.10.5、表42)。ループ構造も名前付き変数の格納もなく、あるのはスタックだけであり、これが準拠したプログラムを理解しやすく保っている——しかしこの制限された演算子集合の中でも、Type 4はType 2やType 3にはできないこと、たとえばDeviceN分版のための本物の複数インク混合式や、条件付き閾値を持つハーフトーンのスポット関数を表現できる。HotPDFの評価器であるHPDFEvalPostScriptCalculatorは、プログラムを一度トークン化する——数値、演算子、そして{ }プロシージャブロックである——その後、ISO 32000-1 §7.10.5が求める深さである100エントリのオペランドスタックを歩き、病的なプログラムや手書きプログラムに対する防御的な歯止めとして評価済み演算子50,000個という固い上限の裏に控える
roll演算子:方向は逆向きに思い込みやすい
rollは、最初の試みで逆向きに出てきやすい演算子である。なぜならその引数の順序も回転方向も、英語がそれを説明する方法とは逆に動くからだ。n j rollはカウントnと回転量jをポップし、スタック上位n個のエントリをj位置分だけ循環的にシフトし、片方の端からあふれた項目をもう一方の端に巻き戻す。仕様そのものから取った標準的な例は、a b c 3 1 rollがc a bを生成するというものだ——最上位の項目がグループの一番下に移動するのであり、その逆ではない。他のすべての項目は場所を空けるために1つずつ上にシフトする。HotPDFの評価器はスタックエントリiの新しい位置を(i + j) mod nとして計算しており、これはこの例と正確に一致するが、これは回転を反転させて書くのも同じくらい簡単な2行のループであり、鏡写しになったrollでもそれらしく見える色は依然として生成される——それは単にファイルの作成者が求めた色ではないだけである
const
Prog = '{ 3 1 roll }'; // (a b c) -> (c a b): the third input moves to the front
var
Reorder: THPDFStreamObject;
begin
// Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
Reorder := Pdf.RegisterPostScriptFunction(
[0,1, 0,1, 0,1], // Domain: 2 numbers per input
[0,1, 0,1, 0,1], // Range: 2 numbers per output (required for Type 4)
Prog);
end;
roundはDelphiのRoundではない:四捨五入対銀行家の丸め
PostScriptのround演算子は、.5のタイを常により大きい整数へ解決するが、Delphi組み込みのRound関数はそうではない:それは0.5への丸めを偶数側へ行う、いわゆる銀行家の丸めの慣習であり、繰り返し丸めた際にバイアスが蓄積しないよう.5のタイがどちらに転ぶかを交互に切り替える。この2つはほとんどの場所で一致するが、ここで重要な境界でちょうど不一致になる——DelphiのRound(0.5)は0を返しRound(2.5)は2を返すのに対し、PDF仕様のroundは同じ入力に対して1と3を求める——そのため、この不一致はカジュアルなテストではすり抜け、計算プログラムの中間計算がちょうど半整数に着地するあらゆる場所で一貫した1ずれとして再現する。ISO 32000-1 §7.10.5の表42は、roundが分数.5をより大きい整数へ押し上げることを明記している。そのためHotPDFはこの演算子をDelphiのRoundを呼ぶのではなくFloor(x + 0.5)として実装しており、Type 4プログラムの算術を手で再実装したり抜き打ちチェックしたりするコードは同じ置き換えを必要とする
function PostScriptRound(const X: Double): Double;
begin
// ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
// integer. Delphi's Round() is banker's rounding and disagrees here:
// Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
Result := Floor(X + 0.5);
end;
登録時の検証が不良な計算機プログラムを早期に捕まえる
不正なType 4プログラムは作成時に捕まえるのは安価だが、他のどこで捕まえるのも高くつく。そのためRegisterPostScriptFunctionはソーステキストを保存するだけではない:関数オブジェクトが文書に書き込まれる前に、宣言された/Domainの中点で一度そのプログラムを試験評価する。不釣り合いな{ }ブロック、認識されない演算子、スタックアンダーフロー、あるいは/Rangeと一致しない出力数はすべてこの試験実行で失敗し、すでに出荷されたファイルでQA中に発見されるレンダリングの不具合ではなく、RegisterPostScriptFunctionの呼び出しを指すコールスタックとともに即座に例外を発生させる。この中点での試験は、そのプログラムが/Domain全体にわたって正しいことを証明するわけではない——入力範囲の片端付近でだけ誤動作する条件分岐は、単一のサンプル点をすり抜けることがある——しかしそれは、単に一つの隅で間違っているだけではなく構造的に壊れているプログラムのクラス全体を締め出す
グラデーションとスポットカラーがこれらの関数をどこで働かせるか
Type 2、3、4は実際のPDFの中で単独で現れることはめったにない。それらは仕様が/Functionキーを受け付けるあらゆる場所に現れ、最も一般的な消費者はシェーディングとスポットカラーの色調変換の2つである。軸方向またはラジアルグラデーションのsh演算子(ISO 32000-1 §8.7.4.5)は、グラデーション軸に沿った位置ごとに一度その/Functionを評価する。これはまさにType 3ステッチングが存在するマルチストップのケースである。SeparationまたはDeviceNカラースペースの色調変換はこの3種類のもう一つの頻出の場であり、そこでType 4がその価値を発揮する:単一のスポットインクは通常Type 2またはType 0の曲線に還元されるが、本物のトラッピングとオーバープリントの挙動を持つ複数インクのDeviceNブレンドは、しばしばPostScript計算機だけが表現できる条件ロジックを必要とする。これはSeparationとDeviceNのスポットカラーのレンダリングに関する記事で扱われているケースである。RegisterSeparationFuncは作成側での対になる呼び出しである:それは着色剤名、代替カラースペース、そしてRegister*Functionファミリーが返す任意のオブジェクトを受け取り、その色調変換をページの他の部分がscn/SCNで選択できるSeparationカラースペースのリソースに組み込む
Type 0のサンプリングされたグリッドとこの3つの数式駆動タイプを合わせると、PDFが宣言できるすべての/Functionをカバーする。どれを選ぶべきかは主に、すでに何を持っているかという問題である:どこか他所で計算されたルックアップテーブルはType 0になり、2つの端点間のブレンドはType 2になり、ドメインにまたがって連結された複数のブレンドはType 3になり、本物の条件ロジックを持つものはすべてType 4になる。RegisterExponentialFunction、RegisterStitchingFunction、RegisterPostScriptFunctionは、そのISO 32000-1関数・シェーディングAPIの残りとともに、DelphiおよびC++Builder向け標準HotPDFコンポーネントの一部である