Delphi向けのlosLab PDFライブラリであるPDFlibPasは、[MS-EMF]の各レコード定義に従って、EMFのPoly*レコードをPDFパスへ変換します。32ビットのEMR_POLYBEZIERは点0から始まり、ポリラインは開いたままストロークのみ、EMR_POLYDRAWのPT_CLOSEFIGUREはフラグであり、すべての点数はレコードサイズに対して検証されます。これらの規則はv3.539.39、v3.539.41、v3.539.43の3リリースにわたって入りました。それ以前は、レポートのチャートがImportEMFFromFileから出てくると、トレンドラインがあるべき場所に塗りつぶされたくさびが現れたり、ベジェ曲線が間違った制御点へ曲がったり、閉じた輪郭の最後の辺が欠けたりしました。どれもエラーを出さず、しかもこの規則は任意のDelphi EMF to PDFコンバーターやGDIレコードパーサーに当てはまります
PDF変換でEMF Poly*レコードがうまくいかないのはなぜか
EMFのPoly*レコードがうまくいかないのは、意味の一部が点の外側にあるからです。図形が開いているか、現在位置から始まるか、どのペンとブラシが適用されるか、レコード内のどこから点が始まるか。拡張メタファイルはデバイスコンテキストへのGDI呼び出しの記録なので、コンバーターは座標だけでなく、そのデバイスコンテキストの状態も再生しなければなりません。PDFにはデバイスコンテキストがありません。あるのはパス、その内側の現在点、そしてストローク(S)、塗り(f)、両方(B)を選ぶ描画オペレーターです。この2つのモデルの食い違いは、すべて沈黙のまま描画の違いになります
Poly*ファミリーには2つの幅もあります。32ビットレコード(例:EMR_POLYLINE)にはそれぞれ、点をSmallIntのペアで格納する16ビットの兄弟(例:EMR_POLYLINE16)が存在します。GDIはすべての座標が収まるときは通常コンパクトな方を記録するので、コンバーターの32ビットハンドラは、日常のテスト描画が一度も到達しないまま、何年も壊れたままでいられます。一番速い監査は、同じ点を両方のレコードに通して、できあがるパスを比較することです。ここで扱うレコードはすべて[MS-EMF]の描画レコードグループ(2.3.5 Drawing Record Types)に入っています
| レコード | 開始点 | 閉じる? | 現在位置 |
|---|---|---|---|
EMR_POLYBEZIER | 点0 | いいえ | 未使用、未更新 |
EMR_POLYLINE | 点0 | いいえ(ペンのみ) | 未使用、未更新 |
EMR_POLYLINETO | 現在位置 | いいえ(ペンのみ) | 使用し更新される |
EMR_POLYPOLYLINE | 各ポリラインの最初の点 | いいえ(ペンのみ) | 未使用、未更新 |
EMR_POLYDRAW | 最初のPT_MOVETO、または現在位置 | PT_CLOSEFIGUREが立っている箇所のみ | 使用し更新される |
EMR_POLYBEZIERの曲線が実際に始まる場所
EMR_POLYBEZIERの曲線は点0から始まり、インデックス1以降の点だけが、制御点、制御点、終点の3つ組にグループ化されます。だから7点のレコードは2つの3次セグメントを描きます。0が始点、1から3が最初のセグメント、4から6が2番目です。PDFlibPasの16ビットハンドラはすでにこうしていました。32ビットハンドラは点0からグループ化を始めたため、始点が最初の制御点として消費され、その後のセグメントがすべて1つずれました。曲線は描けてはいましたが、間違った曲線です。v3.539.41以降は、両方の幅が点0のmでパスを開き、その後の完全な3つ組ごとにcを1つ出します
自前のパーサーへのアドバイスとしては、1+3の倍数でない点数は不正形式であり、末尾の余った点は曲線に縫い付けず無視すべきです
PolyDraw:PT_CLOSEFIGUREは点タイプではなくフラグ
EMR_POLYDRAWでは、PT_CLOSEFIGURE(値1)はPT_LINETO(2)やPT_BEZIERTO(4)と組み合わさるビットなので、型バイトとして有効な値は3や5にもなります。点の型はそのビットをマスクしたバイトで、フラグはこの点で終わるセグメントの後に図形を閉じることを意味します。旧PDFlibPasハンドラはcase文でバイトを単一の値と突き合わせていたため、型3と5の点はどこにもマッチせず、丸ごとスキップされていました。PolyDrawで描いた長方形は閉じ側の辺を失い、最後の点がフラグを運ぶベジェの3つ組はその点を失い、その後の3つ組はすべてずれ込んだままになりました
v3.539.39以降、型はTypes[i] and not PT_CLOSEFIGUREとして読まれ、閉じは完全なセグメントの後でのみ出力されます。閉じ付きPT_LINETOなら線の後、ベジェグループなら3点目の後です。3つ組の1点目や2点目にフラグを立てる不正ファイルがあっても、図形が早閉じされることはありません。同じリリースには関連する修正も2つ入っています:
- 16ビットの
EMR_POLYDRAW16ではPT_MOVETOが来るたびにパス全体が再開され、3つの図形を含むレコードは最後の1つしか残りませんでした。今は最初のムーブがパスを開始し、その後のムーブはサブパスを開きます PT_MOVETOで始まらないPolyDrawレコードは、レコード定義どおり現在位置から始まり、手前にmのないlやcオペレーターを書かなくなりました
EMFポリラインをPDFで塗ってはいけない理由
EMFポリラインを決して塗ってはいけないのは、EMR_POLYLINEとEMR_POLYPOLYLINEがペンのみで描かれる開いた図形であり、PDFで開いたパスを塗ると暗黙的に閉じられるからです。ISO 32000-1 §8.5.3は、塗りオペレーターは描画の前に任意の開いたサブパスを閉じると定めています。だから3点のポリラインにBやfを出すコンバーターは、現在のブラシ色で塗りつぶされた三角形を描いてしまいます。チャートのトレンドラインの下の塗りつぶしくさびの正体です。v3.539.41より前のPDFlibPasは、両方の幅のポリラインをブラシで塗り、32ビットレコードはさらに明示的に閉じていました。今は両方ともストロークだけで終わり、GDIの区別は保たれます。Polygonは閉じて塗り、Polylineは決してそうしません
PolylineToは現在位置から始まる
EMR_POLYLINETOは現在位置からレコード内のすべての点を通って描き、開いたままになり、現在位置を最後の点に置きます。旧ハンドラには、最初の2点がy座標を共有するときにペンをオフにする特殊ケースまで入っていて、それを元に戻すコードはどこにもなかったため、ファイル内のその後のレコードはすべて輪郭を失いました。ペンの状態はEMR_SELECTOBJECTとEMR_CREATEPENの管轄であって、描画レコードのハンドラが触れるものではありません。この特殊ケースはv3.539.41で取り除かれ、レコードの1点形式は自分の点の先を読まなくなりました(v3.539.39で修正)
PolyPolylineの点はカウント配列の後から始まる
32ビットのEMR_POLYPOLYLINEはnPolys個のカウントを格納し、その後にcptl個の点が続きます。点はバイトオフセット32 + nPolys * 4から始まります。罠はRTL側にあります。WindowsユニットはTEMRPolyPolylineを、aPolyCountsとaptlが要素1つの配列であると宣言しているので、aptl[0]が最初の点になるのはnPolysが1のときだけです。aptlを直接インデックスするコードは、複数ラインのレコードすべてでカウント値を座標として読みます。旧PDFlibPasハンドラは境界チェックのサイズもその間違ったレイアウトで計算していたため、有効な複数ラインレコードは拒否され、単一ラインのものは何も描きませんでした。v3.539.41以降、PDFlibPasはPolyPolygonハンドラが昔からそうしていたように、計算したオフセットから点配列を位置特定し、各ポリラインを独自の開いたサブパスとして描き、最後に1回ストロークします。v3.539.43では16ビットの兄弟も同じ扱いになりました。こちらはセグメントごとに描いていたため、線の結合が壊れ、選択済みのNULL_PENが無視されていました
デフォルトのペンとブラシ、そしてパスブラケット
v3.539.43のポリライン修正を締めくくる状態ルールが2つあります:
- 新しいGDIデバイスコンテキストには最初から
BLACK_PENとWHITE_BRUSHが選択されているので、EMR_SELECTOBJECTなしで描くメタファイルでも黒い輪郭が描かれます。コンバーターは以前、ペンも塗りもない状態で始め、その種のレコードにn(パスを終え、何も描かない)を書いていました BeginPath/EndPathブラケットの内側では、Polylineは現在位置を使わず更新もしません。だから前の図形に接続するのでなく、最初の点で新しいサブパスを開かなければならず、ブラケットがストロークか塗りで使われるまで何も描いてはいけません
TMetafileCanvasでEMFテストファイルを作る
コンバーターをこれらの規則に対して検査する一番速い方法は、3つの危険な呼び出しをTMetafileCanvasで1つの拡張メタファイルに記録することです。下の描画コードは、曲線を中空ブラシで記録してから、ポリラインのためにわざとソリッドの黄色ブラシを選びます。正しいコンバーターならポリラインに対してそのブラシを無視するはずで、出力PDFに黄色があればバグです。PolyDrawにはTCanvasラッパーがないので、キャンバスハンドルを使ってWindows API経由で呼び出し、型バイト3と5で閉じフラグを試します
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// 閉じた正方形(3 = LINETO + CLOSEFIGURE)、続いて閉じたBezier図形
// 最後の制御3つ組が 5 = BEZIERTO + CLOSEFIGURE で終わるもの
DrawPts: array[0..7] of TPoint = (
(X: 300; Y: 40), (X: 380; Y: 40), (X: 380; Y: 120), (X: 300; Y: 120),
(X: 420; Y: 120), (X: 440; Y: 40), (X: 520; Y: 40), (X: 540; Y: 120));
DrawTypes: array[0..7] of Byte = (
PT_MOVETO, PT_LINETO, PT_LINETO, PT_LINETO or PT_CLOSEFIGURE,
PT_MOVETO, PT_BEZIERTO, PT_BEZIERTO, PT_BEZIERTO or PT_CLOSEFIGURE);
var
Mf: TMetafile;
Canvas: TMetafileCanvas;
begin
Mf := TMetafile.Create;
try
Mf.Enhanced := True;
Mf.Width := 600;
Mf.Height := 260;
Canvas := TMetafileCanvas.Create(Mf, 0);
try
Canvas.Pen.Color := clNavy;
Canvas.Pen.Width := 2;
Canvas.Brush.Style := bsClear; // 曲線は輪郭のみ
// 点0が始点。1..3と4..6が2つの3次セグメント
Canvas.PolyBezier([Point(20, 120), Point(60, 20), Point(100, 220),
Point(140, 120), Point(180, 20), Point(220, 220), Point(260, 120)]);
PolyDraw(Canvas.Handle, DrawPts[0], DrawTypes[0], Length(DrawPts));
// ソリッドブラシ選択中の開いたV字。ストロークのみで
// 黄色の三角形には閉じない
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // 記録を終える
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
これらの座標はSmallIntに収まるので、GDIは通常16ビット版を記録します。32ビットハンドラに到達するには、それを書き出すプロデューサーか、手で組み立てたレコードが必要です。手組みのファイルには固有の罠もあります。VCLのTMetafile.LoadFromStreamは、残り長さが108バイトのTEnhMetaHeaderより厳密に大きいときにだけ、ストリームをEMFとして扱います。短いヘッダーの手書きEMFや、ちょうど108バイトの空のEMFはWMFとみなされ、"Metafile is not valid"で拒否されます。テストレコードの前には、拡張フィールドを含む完全な108バイトのヘッダーを必ず書いてください
PDFlibPasでEMFをPDFへインポートする
PDFlibPasはImportEMFFromFileかImportEMFFromStreamでEMFをインポートします。成功時は非ゼロの画像IDを返し、失敗時は0です。GeneralOptions = 0は、この記事の主題であるベクターパスを保持します。1は代わりにメタファイルをビットマップへラスタライズします。FontOptions = 1は、メタファイルのフォントを非埋め込みTrueTypeフォントとして追加します。ストリーム版はロード前にストリームを位置0へ巻き戻すので、メタファイルだけを含むストリームを渡してください
uses
System.SysUtils, PDFlibrary;
procedure EmfToPdf(const EmfFile, PdfFile: WideString);
var
PDF: TPDFlib;
ImageID: Integer;
PageOps: AnsiString;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1); // DrawImage用の左上原点
PDF.SetMeasurementUnits(0); // ポイント
// FontOptions 1 = フォントを非埋め込みTrueTypeとして追加
// GeneralOptions 0 = ベクターインポート、1 = ビットマップ
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// EMFの場合、ImageWidth / ImageHeightはポイント単位のフレームサイズ
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// ページは取り込んだフォームを呼ぶだけ:q ... cm /Name Do Q
PageOps := PDF.GetPageContentToString;
if Pos(AnsiString(' Do'), PageOps) = 0 then
raise Exception.Create('Expected a form XObject invocation');
if PDF.SaveToFile(PdfFile) <> 1 then
raise Exception.Create('The PDF could not be saved');
finally
PDF.Free;
end;
end;
ベクターのEMFインポートはフォームXObjectになるので、GetPageContentToStringが返すのはセーブ、変換、Do、リストアのシーケンスだけです。Poly*レコードから生成されたm、l、c、h、Sの各オペレーターは、圧縮されたフォームXObjectストリームの中に住んでいます。監査するには、保存したファイルをPDFオブジェクトインスペクターで展開し、フォームストリームを読みます。上のテストファイルなら、ポリラインが手前にhなしでSで終わること、PolyDraw図形の閉じフラグごとにhがあること、これらのサブパスにfやBが一切ないことを確認するはずです。DrawImageは取り込んだEMFをWidthとHeightの小さい方で一様にスケールするので、渡すボックスが縦横比と合っていなくても、描画はアスペクト比を保ちます
Free Pascalターゲットについては、PDFlibPasのEMFベクターインポーターがFree Pascalでどうビルドされるかを参照してください。インポーターがどこでコンパイルされようと、レコードのセマンティクスは同じです
EMFパーサーはファイル由来の点数をどう扱うべきか
EMFパーサーは、すべての点数を信頼できない入力として扱い、1点でもコピーする前にレコードサイズに対して検証すべきです。EnumEnhMetaFileが保証するのは、各レコードのnSizeがファイル内に収まることだけです。cptlがnSizeと整合するかは見ていません。だからカウントが偽造・破損していたとき、Moveでcptl点をコピーするハンドラは、後続のレコードやメタファイルの末尾の先まで読みに行きます。v3.539.39以降、PDFlibPasはPolyDraw、PolyBezier、PolyBezierTo、Polyline、PolylineTo、Polygonの両方の幅について、固定ヘッダー+点数×点あたりバイト数をnSizeに対して検査し、PolyDrawの型バイトは1点あたり1バイト余分に数えます。PolyPolyレコードでは、図形ごとのカウントの合計が宣言された総数を超えないことも要件で、0点の図形はスキップされます
同じ検査は、自前のパーサーにコピーできるほど短いものです。このバージョンは32ビットのEMR_POLYPOLYLINEを検証し、実際の点配列へのポインターを返します:
uses
Winapi.Windows;
// レコードが宣言どおりの点を本当に保持する場合を除きnilを返す
// 点はカウント配列の後から始まる:32 + nPolys * 4 バイト先であり
// RTLが要素1つの配列と宣言するaptl[0]ではない
function PolyPolylinePoints(Rec: PEnhMetaRecord): PPoint;
var
P: PEMRPolyPolyline;
Count: PDWORD;
PointsOffset, Total: Int64;
I: Cardinal;
begin
Result := nil;
if (Rec^.iType <> EMR_POLYPOLYLINE) or (Rec^.nSize < 32) then
Exit;
P := PEMRPolyPolyline(Rec);
if P^.nPolys = 0 then
Exit;
PointsOffset := 32 + Int64(P^.nPolys) * SizeOf(DWORD);
if PointsOffset + Int64(P^.cptl) * SizeOf(TPoint) > Rec^.nSize then
Exit; // 偽造または切り詰められたカウント
Total := 0;
Count := @P^.aPolyCounts[0]; // ポインターで歩く:[0..0]は範囲チェックに引っかかる
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // 図形が実在より多くの点を要求している
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
点が収まるかの検査が先に走るので、ループが歩く時点でカウント配列がレコード内にあることは確かめられています。算術がInt64なのは、32ビットで計算したnPolys * 4やcptl * 8はラップアラウンドして比較をすり抜け得るからです
クイックリファレンス:EMF to PDF変換のためのEMF Poly*規則
EMR_POLYBEZIER:点0が始点。点1から3つ組でグループ化。32ビットレコードはv3.539.41で修正EMR_POLYLINE/EMR_POLYPOLYLINE:開いた図形。Sでストロークし、h、f、Bは決して使わない。PDFの塗りは開いたサブパスを閉じるためEMR_POLYLINETO:現在位置から始め、開いたままにし、現在位置を更新し、ペンの状態には決して触れない- 32ビットの
EMR_POLYPOLYLINE:点はバイト32 + nPolys * 4から始まり、aptl[0]ではない EMR_POLYDRAW:振り分け前にPT_CLOSEFIGUREをマスク、完了したセグメントの後で閉じる、最初の点がPT_MOVETOでなければ現在位置から始める- デフォルトのデバイスコンテキスト状態は
BLACK_PEN+WHITE_BRUSH。v3.539.43以降はこれを尊重する BeginPath/EndPathの内側では、各ポリラインが自分のサブパスを開き、ブラケットが使われるまで何も描かれない- 点をコピーする前に、すべての
cptl/cptsを64ビット算術でnSizeに対して検証する - 手組みのテストEMFには完全な108バイトのヘッダーが要る。さもないと
TMetafile.LoadFromStreamはWMFとして読む
レポートが別のコンポーネントを通る場合も、レコードのセマンティクスは同じです。HotPDFのEMFとWMFベクターインポートは、あちらのコンポーネントがグラデーションやハッチブラシをどうPDFパターンに変えるかを扱い、PDFlibPasのベクターグラフィックス、シェーダー、グラデーションは、メタファイルを経由せずライブラリAPIで直接同じ図形を描く方法を扱います
上記の規則はすべて、PDFlibPas v3.539.43以降に入っています。詳細とトライアルダウンロードは、PDFlibPas Delphi PDF library製品ページにあります