PDF ページに置いた EPS ロゴは、輪郭が消えた状態で出てくることがあります。単色のマークが自分自身のネガとして印字されたり、Æ が á に化けたりもします。症状のどれも、PostScript と PDF のグラフィックス意味論が食い違う場所を示しています。だから Delphi 向け PDFlibPas の EPS インポートは、PostScript プログラムを解釈し、それぞれの差をネイティブな PDF 演算子として書き出します
プログラムを解釈するか、素通しするか
PostScript は PDF の中に 2 通りで座れますが、画面に何かを見せるのは 1 つだけです。ISO 32000-1 §8.8.2 は PostScript XObject を定義します。/Subtype /PS ストリームで、そのバイトは、出力デバイスが PostScript プリンターのときに限り、解釈されずに印刷ジョブへコピーされます。それ以外では何の効果もなく、§8.8 の概観は PostScript XObject を使うべきでないと言います。PDF/A は丸ごと排除します。TPDFlib.NewPostScriptXObject と DrawPostScriptXObject はその素通し形式を書き、ImportEPSFromFile と DrawPostScript はプログラムをインタープリターに通し、普通のパス、インライン画像、テキストを出します。アートワークはインタープリターへ通し、PS XObject は、印刷室の 1 台の RIP、つまりプレスやプロダクションプリンターの手前のラスタイメージプロセッサを狙う断片のために取っておきます。EPS の解釈は、EMF ベクトル取り込みとも別の仕事です。そちらの入力は再生すべき GDI レコードのリストです。EPS ファイルはプログラムです
procedure AddPrintRoomHook(PDF: TPDFlib);
var
PSRef: Integer;
begin
// /Subtype /PS ストリームとして一字一句そのまま格納され、Do で参照される
// PostScript プリンターでのみ実行される。ビューアは何も描かない
PSRef := PDF.NewPostScriptXObject(
'userdict /PrintRoomMark known { PrintRoomMark } if');
if PSRef <> 0 then
PDF.DrawPostScriptXObject(PSRef);
end;
| PostScript の振る舞い | PDF の振る舞い | 文字どおりの翻訳の結果 |
|---|---|---|
gsave / grestore は現在のパスと現在点を保存し復元します | q / Q はパスに触れず、描画がパスを終わらせます(§8.5.1) | grestore のあとの輪郭や続きのセグメントが消えます |
clip は現在のパスを後の描画のために保持します | W の後には描画演算子、たいてい n が来て、パスを終わらせます | クリップの後のストロークは描くものを持ちません |
ImageMatrix はユーザー空間を画像空間へ写像します | すべての画像は単位正方形を満たし、最初の行が上に来ます(§8.9.4) | ボトムアップや回転した画像は反転したり場所を外したりします |
true imagemask は 1 のビットを塗ります | 既定の Decode [0 1] を持つステンシルは 0 のビットを塗ります(§8.9.6.2) | 単色のロゴはネガとして印字されます |
| Roman の Standard-14 フォントは既定で StandardEncoding です | StandardEncoding という名に特別な意味はありません(§9.6.6.1) | アクセント、引用符、合字が文字を変えます |
%%BoundingBox と %%HiResBoundingBox コメントがアート領域を定義します | 配置はライターが出す行列のままです | アートは意図されたボックスからずれたり、伸縮したりします |
grestore のあと、輪郭はどこへ消えたのか
輪郭が消えたのは、PDF がパスを一度も保存しないからです。ISO 32000-1 §8.5.1 はそれを 1 文に圧縮しています。「PostScript と違い、PDF では現在のパスはグラフィックス状態の一部ではない」。そしてパスが描画されたあとは、m か re が新しいパスを始めるまで、現在のパスはありません。PostScript のプロデューサーは逆の規則に頼っています。塗りと輪郭を持つ図形を描く定石は gsave fill grestore stroke です。塗りが保存されたパスのコピーを消費し、grestore がパスを取り戻し、ストロークが同じジオメトリを描きます。gsave を q に、fill を f に、grestore を Q に対応させると、ストロークは空のパスとともにやってきます。図形は塗られ、輪郭は消えます。復元されたパスを続ける grestore の後の lineto も同じように失敗します。欠けたセグメントとして
uses
SysUtils, PDFlibrary;
const
// 輪郭付きの塗り三角形:塗りは gsave/grestore の中で走り、
// grestore の後のストロークは復元されたパスを再利用する
TriangleEPS =
'%!PS-Adobe-3.0 EPSF-3.0'#10 +
'%%BoundingBox: 0 0 200 120'#10 +
'newpath 20 20 moveto 180 20 lineto 100 100 lineto closepath'#10 +
'gsave 0.85 setgray fill grestore'#10 +
'3 setlinewidth 0 setgray stroke'#10;
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1); // Left と Top は左上の角から測る
// Width と Height の 0 は 200 x 120 のバウンディングボックスサイズを保つ
if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
PDF.SaveToFile('triangle.pdf')
else
Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
// 期待結果:3 単位の黒の輪郭を持つ薄いグレーの三角形。
// 文字どおりの q/f/Q/S 翻訳は三角形を塗り、輪郭を落とす
finally
PDF.Free;
end;
end.
PDFlibPas は自分の PostScript パスを保持します。その瞬間に PDF コンテンツストリームが何を保持していようと、それは別物です。そして PDF のパスを書くのは、何かが描くかクリップするときだけです。各点は、moveto や lineto が走ったときに効いていた変換行列の下で固定されます。PostScript と同じで、後の scale や concat は、パスに既に載った点を決して動かしません。currentpoint は保存された点を現在の行列を通して逆写像するので、今のユーザー空間で答えます。gsave は、パスチェーンの終端への参照と、現在点、行列、フォント、色空間を記録します。だからネストした保存がパス全体をコピーすることはありません
クリッピングは、2 つのパスモデルがまた分かれる場所です。そして取り込みはここでも PostScript に従います。clip と eoclip は PDF 側に W n を出す一方、よく続くストロークのために論理パスをその場に残します。PostScript で新鮮な空のパスを始める rectclip は re W n を出し、パスをクリアします。パスの保存は 100,000 ノードで頭打ちされ、再生は共有の演算予算を使うので、敵意あるファイルが gsave と stroke をループして出力を膨らませることはできません
画像の配置:ImageMatrix は逆の方を指す
EPS の画像は、ImageMatrix を配置として扱うと反転して着地します。それが記述するのは逆向きの写像だからです。PDF では §8.9.4 が画像の境界をユーザー空間の単位正方形に結び付けます。最初のサンプル行が上で、暗黙の画像からユーザーへの行列は [1/w 0 0 -1/h 0 1] です。PostScript の ImageMatrix はユーザー空間を画像空間へ写像します。だから PDF の配置行列は、[w 0 0 -h 0 h] に ImageMatrix の逆行列を掛けたものです。よくあるトップダウンの行列 [w 0 0 -h 0 h] では、積は単位行列です。素朴なコンバータが日常の使い方では正しく見えるのはこのためです。[w 0 0 h 0 0] で書かれたボトムアップのスキャンは [1 0 0 -1 0 1]、つまり単位正方形の中での縦の反転を与え、回転や歪みの行列は特別扱いを要しません。有限のデータソースが早く終わったとき、PDFlibPas は完成した行だけを塗り、元の ImageMatrix を保ち、実際に届いた行で伸縮します。切り詰められた画像は正しいスケールでその領域の上の帯を満たします。生き残った行を全高に引き伸ばすのは、無傷に見える歪んだ絵を作るだけです
imagemask はどちらのビットを塗るのか
極性オペランドに true を取った imagemask は 1 のビットを塗ります。PDF ではそれが Decode [1 0] です。ISO 32000-1 §8.9.6.2 は [0 1] を画像マスクの既定と定義します。0 のサンプルが現在の色でページに印をつけ、1 は背景をそのままにするという意味で、[1 0] はその逆です。PostScript のブール値が言うのは、どちらのサンプル値が塗るかです。「PDF の既定を反転せよ」と読むと、ロゴはネガとして印字されます。マークを切り抜いた固まりです。PDFlibPas は true を [1 0] に、false を [0 1] に対応させ、ステンシルを /IM true 付きのインライン画像として出します。プログラムが設定したどんな色でも塗れるようにです。そして辞書形式では、その 2 つの Decode 配列だけを受け付けます
12 ビットのサンプルは、もっと小さな不一致を隠しています。PDF に 12 ビットの画像はありません。§8.9.5.1 が許すのは 1、2、4、8、そして PDF 1.5 から 16 ビット每コンポーネントです。各サンプルを左へ 4 ビットシフトすれば何も失わずに 16 ビットに収まります。それでも最大値は 65535 ではなく 65520 になるので、既定の Decode では明るいサンプルがどれも約 0.023 パーセント暗く出ます。取り込みはシフトを保ち、Decode の上端を 65535/65520 だけ伸ばし、すべての正規化値を復元します。12 ビットの画像は出力バージョンを自動で PDF 1.5 に引き上げます。PDF/A や PDF/X モードでは、あるいは出力バージョンが 1.5 未満に固定されているときは、取り込みはページが変わる前にエラー 538 で止まります
Æ が á になるのはなぜか
PostScript の Roman フォントは、プログラムが再エンコードしない限り StandardEncoding を使います。そして PDF はそのテーブルを名指せません。ISO 32000-1 §9.6.6.1 はこの名前に特別な意味を与えず、WinAnsiEncoding、MacRomanEncoding、MacExpertEncoding を事前定義の選択肢として残します。Annex D.2 のテーブルは、コードを変更なしに WinAnsi へ渡したとき何が起きるかを示しています。8 進 341(0xE1)は StandardEncoding では AE、WinAnsi では aacute なので、Æ は á になります。0xAE は片方では fi 合字、もう片方では ® です。0x27 は StandardEncoding では quoteright、WinAnsi ではまっすぐな quotesingle で、タイポグラフィのアポストロフィがタイプライター式に変わってしまいます。プログラムが使う Standard-14 の Roman フォントごとに、PDFlibPas はプライベートな Type 1 フォントリソースを書きます。その /Encoding 辞書は /BaseEncoding /WinAnsiEncoding と、StandardEncoding 順にすべての 256 コードを名指す /Differences 配列を持ちます。だからどのコードも、ビューアがベーステーブルをどう埋めるかに依存しません。幅は同じエンコーディングから来て、ToUnicode マップでコピーと検索が Æ を返し、Symbol と ZapfDingbats は組み込みのコードを保ちます。取り込みの前にコードが選んだフォントは触られません。EPS に合わせてキャッシュされたフォントを再エンコードするのは、それで既に置かれたテキストを変えることになるからです
これらのリソースはフォントを埋め込まずに名指します。埋め込みを要求するプロファイルとは衝突します。PDF/A、PDF/X、PDF/UA のために設定された文書では、あるいは SetEmbedAllFonts(1) のあとは、EPS テキストはエラー 538 で失敗し、ページはそのままにされます。アーカイブ出力には、テキストをアウトラインに変換して書き出した EPS がきれいな道です。どのモードでも素のパスとして取り込めます。既存のファイルに未埋め込みのまま残されたフォントには、代わりに PDF/A 向けの未埋め込みフォントの埋め込みが必要です。EPS テキストは 14 の標準フォントに正確な名前で対応します。自分のフォントプログラムを埋め込むアートワークは、テキストがアウトライン化されればきれいに取り込めます。ローカルの代替フォントがすべての行を動かしてしまうことも防げます
バウンディングボックス、DSC コメント、バイナリ EPS ファイル
配置はアートのバウンディングボックスを、渡された矩形へ伸縮します。だからボックスは正しい場所から来ねばなりません。PDFlibPas は %%BoundingBox と %%HiResBoundingBox を、本物の DSC コメント行、つまり行を開始するトップレベルのコメントからだけ読みます。有効な高解像度ボックスは整数のボックスに勝ちます。素朴な部分文字列検索なら、リテラル文字列の中、手続き本体の中、バイナリ画像データの中でも、そのキーワードを見つけてしまいます。EPS ファイルは 3 つすべてを運びます。ヘッダー値の (atend) は数値をトレーラーの中のコメントへ先送りします。スキャナーが届いたときに読みます。ボックスがまったくないファイルは 612 × 792 にフォールバックし、空のボックスや逆転したボックスはエラー 124 で失敗します。Width と Height はボックスを独立に伸縮します。自然なサイズを保つには両方に 0 を渡すか、ボックスのアスペクト比で両方を渡します。片方だけの非ゼロ値は、アートを 1 つの軸に沿ってだけ引き伸ばします
バイナリデータが入り口を決めます。DrawPostScript は WideString を取ります。コードの中で組まれた PostScript には十分です。しかし生の画像サンプル、currentfile を通じて読まれる Flate データ、DOS EPS ラッパーは、Unicode テキストの旅を生き延びられないバイトを含みます。ImportEPSFromFile はファイルを生のバイトとして読み、C5 D0 D3 C6 のプレビューヘッダーを認識し、PostScript セグメントのオフセットと長さがファイルの中に収まることを確認し、TIFF や WMF のプレビューを無視してそのセグメントだけを実行します。どちらの呼び出しも最大 4 MiB のソースを取り、Options = 0 を要求し、1 か 0 を返し、描画の全体か、何もかのどちらかを追記します。失敗の理由は LastErrorCode に座っています。読むとリセットされるので、1 度だけ受け取ってください
uses
SysUtils, PDFlibrary;
function PlaceEPS(PDF: TPDFlib; const FileName: string;
Left, Top: Double): Boolean;
var
Code: Integer;
begin
// 0, 0:%%HiResBoundingBox か %%BoundingBox からの自然サイズ
Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
if Result then
Exit;
Code := PDF.LastErrorCode; // 読むとコードはクリアされるので、1 度だけ読む
case Code of
402:
Writeln(FileName, ': file cannot be opened');
PDFLIB_ERROR_POSTSCRIPT_INVALID: // 124
Writeln(FileName, ': malformed PostScript, bad bounding box or nothing painted');
PDFLIB_ERROR_POSTSCRIPT_UNSUPPORTED: // 538
Writeln(FileName, ': construct the import does not handle, or Standard-14 text in an archival profile');
PDFLIB_ERROR_POSTSCRIPT_BUDGET: // 539
Writeln(FileName, ': source over 4 MiB or a work limit reached');
else
Writeln(FileName, ': import failed with code ', Code);
end;
end;
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1);
if PlaceEPS(PDF, 'logo.eps', 40, 40) then
PDF.SaveToFile('letterhead.pdf');
finally
PDF.Free;
end;
end.
エラー 538 が演算子を名指したら、アートをエディターから、テキストをアウトラインに変換した素の EPS として再書き出しください。演算子を追いかけるよりたいてい速く、アウトライン化されたアートは、PDFlibPas Delphi PDF Library で作った PDF/A や PDF/UA の文書にも、Standard-14 フォントの衝突なしで置けます