PDFlibPas(PDF Library for Delphi)は、ファイルを書く前にすべてのオブジェクトをPDFバージョン規則のテーブルと照合します。そのバージョンプリフライトは最近まで、普通のCAD測量辞書を地理空間のものと取り違えていました。1ページのCAD図面は問題なく読み込めたのに、SaveToFileが0を返し、LastErrorCodeは602で、1.7 ExtensionLevel 3を要求してきました。修正後の規則は、直線測量の/Measure辞書(/Subtype /RL)を素のPDF 1.6として扱い、拡張のゲートは本物の地理空間マーカーのために取っておきます
ファイルはコーパス受理を通ってやってきました。1ページ、optional-contentグループ1つ、直線測量のビューポート2つ。建築CADパッケージが、ビューアーに平面図から距離を読み取らせるために書く、典型的な出力です。変わったところは何もなく、だからこそ拒否が問題だったのです。有効なファイルを止めるプリフライトは、遅いプリフライトより悪質です。呼び出し側は、文書が持っていない機能を指す、権威ありげな診断を受け取るのですから。修正は2つの部分からできています。1つの規則の背後にある仕様の読み方と、その規則が探していたレベルでは2つの辞書型を見分けられなかったという気づきです
PDFlibPasの保存時バージョンプリフライトの仕組み
保存のゲートであるPrepareAndCheckSaveVersionは、すべての間接オブジェクトをPDFFeatureRulesと照合し、マッチして、かつターゲットが許すより多くを要求する最初の規則で失敗します。ターゲットとは、文書バージョン(またはLockSaveVersionで固定されたバージョン)に、/Extensions /ADBEの下で宣言されたAdobe拡張レベルを足したものです。各TPDFFeatureRuleレコードは、MinVersion、MinExtensionLevel、fmkDictKeyやfmkDictSubtypeのようなMatchKind、Match文字列、人間に読めるFeature名、省略可能なコールバックを運びます。AddRuleは素のバージョン規則を登録し、AddExtensionRuleは常にMinVersionを17に固定したうえで拡張レベルを乗せるので、拡張規則を満たせるのはPDF 1.7+正しい/Extensionsエントリーだけです。ゲートが引っかかると、要求バージョンと機能名が呼び出し側のために保持され、GetInformationのキー311、312、313がそれを公開します
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311:要求バージョン、312:それを引き起こした機能、
// 313:保存ターゲットがロックされているバージョン(未ロックなら'')
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
普通のCAD図面がエラー602で失敗したのはなぜか
規則テーブルにはAddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil)が入っていました。これは、単に/Measureキーを持つ任意の辞書で発火します。そして測量ビューポートはどれもこのキーを持っています。ページの/VP配列がビューポート辞書を保持し、各ビューポートは/Measure経由で自分のmeasure辞書を指す。キー存在のマッチはそこで止まり、measure辞書が実際に何であるかは見ませんでした。読み込み時の機能スキャンは文書バージョン番号を1.7へ引き上げられますが、入力ファイルの代わりに/Extensions宣言を書くことは決してないので、保存ゲートは拡張レベル0のPDF 1.7を見て、1.7 ExtensionLevel 3と報告しました。拡張宣言をでっち上げないこの態度は意図的です。ライブラリーは、間違った規則を紙で覆うために、入力ファイルを静かに昇格させたりしません
仕様は直線測量のケースについて曖昧さを残していません。Measure辞書はPDF 1.6で入りました。ISO 32000-1 §12.9は/Subtypeの既定値をRLとし、これは固有のエントリー群で記述される直線座標系です。縮尺比、XとYの数値形式、距離、面積。地理空間測量はその後の追加で、PDF 1.7のうえのAdobe Extension Level 3から来ており、/Subtype /GEOで識別され、地理的な点の配列、座標系辞書、表示単位を運びます。DelphiでGeoPDFビューポート、GPTS、LPTS配列を読むで歩いた構造です。両方の辞書が同じ/Measureキーにぶら下がるので、キーで止まる規則は両方に正しくあり得ません。見分けに必要な情報は1階層下、measure辞書そのものの中にあります
修正後の規則セットがそれでも強制するもの
修正は無条件のキー規則を削除し、実際のバージョン要求を記述するゲートを残します。/VPや/UserUnitを持つページは引き続きCB_PagePDF16Entries経由でPDF 1.6を必要とし、/PtDataキーは引き続き拡張レベル3を必要とし、CB_GeospatialDictionaryは、届いたキーではなく中身によって、measure辞書が地理空間かどうかを判定します
// 削除:/Measureキーを持つ全辞書が地理空間として数えられていた
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
共有のDelphi/FPCリグレッションは、その境界を両側から固定します。measure辞書が/Subtypeを省いたビューポートも、/RLと明記したビューポートも、PDF 1.6で通ります。同じページはPDF 1.5では依然として拒否され、機能検出はもう拡張を報告しません。/GPTS配列を加えると判定は1.7 ExtensionLevel 3へ戻り、拡張レベルを宣言すれば通ります。裸の/Subtype /GEO辞書は宣言なしでは拒否されます。コールバックは設計上、保守的です。迷子の/GCSや/PDUキーを運ぶ直線辞書は地理空間として扱われます。RLモデルではこれらのキーに意味がないからです
LockSaveVersionは、この変更が呼び出し側に見える場所です。TPDFlib.LockSaveVersionは'1.0'から'1.7'までを受け付け、それ以外は0を返し、文書バージョンを固定し、ライター側の呼び出しが黙って引き上げるのを止めます。それでも保存ゲートは固定された値に対して走ります。修正後の規則では、1.6に固定されたCADファイルはきれいに保存されます。本物のGeoPDFが1.6に固定されていれば、やはり602です。これが正しい答えです。そしてSetMeasureDictCoordinateSystemのような地理空間のオーサリング呼び出しは、API経由でその内容を作るとき、拡張レベル3を自分で宣言します
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 1.6を超える本物の内容、たとえばGEOのmeasure辞書
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
バージョン規則スキャンが必要以上に遅かったのはなぜか
スキャンはテストの前に、すべてのTPDFFeatureRuleをローカルのレコードへコピーしていました。レコードはAnsiStringフィールドを2つ持つので、コピーのたびに参照カウントが2つ調整され、前の値が解放されます。プリフライトはすべてのオブジェクトツリーのすべてのノードを、スカラーも含めて訪れるので、そのコストはオブジェクト数×規則数で積み上がります。ターゲットバージョンに適用すらされない規則も、まずコピーされて、その後で飛ばされるわけです。PDFFeatureRulesはユニット初期化時に一度だけ満たされ、読み取り専用として扱われるので、v3.539.17はテーブルのエントリーをMatchSingleRuleとRuleExceedsTargetへ直接渡します。この2つのconst Ruleパラメータは、文字列に触れずに参照を受け取ります
// 以前:規則ごと、訪れたオブジェクトごとのマネージドレコードコピー
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// 以後:constパラメータが不変のテーブルエントリーをその場で読む
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
測定された効果は狭く、そのように引用されるべきです。ベンチマークは20,000個の数値オブジェクトの配列を、PDF 1.4のターゲットに対してラウンドごとに10回照合します。-O2付きのFPC Win64ビルドで、5ラウンドの中央値は0.711秒から0.203秒へ下がり、2つのビルドを逆順に実行すると0.459秒対0.150秒でした。これは規則マッチングの経路だけのおおよそ3倍の利得です。実際の保存は遅延機能検出、オブジェクトのデコード、直列化にも払うので、この比率は保存時間全体には持ち越されません。規則の順序、コールバック、バージョンの閾値、最初の失敗の診断は変わらず、ここに至るために規則が保存をまたいでキャッシュされたり、飛ばされたりしたこともありません
読み込んだPDFがバージョンプリフライトに落ちたとき何を確認すべきか
バージョンに触れる前に、キー311と312を読んでください。機能名が地理空間辞書を指し、ファイルが描いているのが直線測量だけなら、それはこの誤陽性で、現行のビルドはファイルを無変更のまま保存します。機能が本物なら、拡張を宣言するか、内容を正直に含むバージョンに固定します。ゲートを黙らせるためだけにバージョンを引き上げると、出荷したものを下流の消費者が読めるかという問いが隠れます。境界があり証拠に裏打ちされた検査という同じ原則は、エンジニアリング文書向けのPDF/E-1オーサーモードプリフライトを支えています。CAD図面がバージョン番号ではなく適合標準と向き合う場所です
バージョン適合の検査、測量と地理空間の辞書、そして保存バージョンのロックは、いずれもDelphi、C++Builder、Lazarus開発者向けのPDFlibPasツールキット、PDF Library for Delphiの一部です