グリフ幅が埋め込みフォントプログラムと食い違っているとだけ言うveraPDFのレポートは、どのグリフか、なぜかという点をほとんど教えてくれません。PDFlibPasはこの問いに対して、各文字コードを埋め込みcmapでグリフインデックスへ解決し、プログラムのメトリックをemあたり1000単位へ正規化し、そこで比較することで答えます
グリフ幅はなぜ食い違うのか
比較される二つの数が異なる座標系に住んでおり、PDF辞書のどこにも変換を告げるものがないからです。フォント辞書は/Widthsをグリフ空間で書き、PDFはこれをemの千分之一に固定しています(ISO 32000-1 §9.2.4)。埋め込みTrueTypeプログラム内のhmtxテーブルは送り幅をフォント設計単位で書き、headテーブルがそのいくつでemになるかを決めます。ほとんどのTrueType書体で2048、CFF由来で1000、ときには全く別の値です。生の値を比較すれば、コーパスのすべての2048-upemフォントが壊れて見えます。辞書フィールドを読んで幅を監査しようとする人に、ISO 14289-1 §7.21.5が仕掛ける罠がこれです
PDFlibPasはロード時に正規化します。TPDFTrueTypeParserは幅配列にAdvance * 1000 div unitsPerEmを格納するため、Parser.GetWidth(GID)はすでにPDFが使うのと同じem千分之一で答え、設計単位に戻す必要が生じたときにはGetRawWidthが引き続き使えます。それでも難しい方の半分は残ります。文字コードからグリフインデックスへの到達です。単純TrueTypeフォントの経路は、FontDescriptorのSymbolicフラグ、すなわち/Flagsのビット3で決まります
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// シンボリック体はプログラムのcmapで直接アドレスされ、
// (3,0)の上位バイト規約がフォールバックになる
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// 非シンボリック: コード -> エンコーディング経由でグリフ名、名前 -> Adobe
// Glyph List経由でUnicode、Unicode -> プログラムのcmap経由でGID
UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
if UnicodeValue = 0 then
Continue;
GID := Parser.GetGlyphIndex(UnicodeValue);
end;
if (GID > 0) and (GID < Parser.GlyphCount) then
if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
Inc(MismatchCount);
finally
Parser.Free;
end;
この断片には重みを持つ詳細が二つあります。許容差はゼロではなく一単位です。正規化は整数除算であり、正当に生成されたファイルでも一単位ずれ得るからです。これこそ診断10036が報告する「emの千分之一以内」という文言です。そしてGID < Parser.GlyphCountのガードは飾りではありません。GetWidthはレンダリングの呼び出し側に寛容であるよう書かれており、範囲外のインデックスをhmtxの最終エントリへクランプし、テーブルが不在のときは750へフォールバックします。寛容はレンダリングには正しく、監査には間違っています。そこで監査はクランプを信用せず、幅を尋ねる前にインデックスを拒否します
CIDFontType2はもう一段の間接参照を加える
PDFlibPasは複合フォントも同じように歩みますが、CIDとグリフの間に/CIDToGIDMapが差し込まれます。幅は/W配列で届きます。ISO 32000-1 §9.7.4.3はこれに、一つの配列の中で自由に交互に現れる二つの形を与えています。開始CIDに続けて連続する幅の配列、あるいは最初のCID、最後のCID、そしてその区間にわたって適用される単一の幅です。監査は両方を解析し、でき上がったすべてのペアを同じ比較へ回し、合計を診断10037の下で報告します。マッピングの段階こそ複合フォントが異なるところであり、だからこそ幅を一つも読む前に欠落マップ診断10021が重要になります。不在または不正形の/CIDToGIDMapは、§7.21.3.2に違反するだけでなく、幅の問いに答えられなくするのです
// /CIDToGIDMapは/Identityという名前か、CIDごとに一つのビッグエンディアン
// 16ビットグリフインデックスのストリーム(ISO 32000-1 9.7.4.2節)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
GID := CID;
Result := True;
end
else if Obj is TPDFStream then
begin
Data := TPDFStream(Obj).GetDecodedStream;
P := CID * 2 + 1; // Pascal文字列は1始まり
if (P >= 1) and (P + 1 <= Length(Data)) then
begin
GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
Result := True;
end;
end;
フォントプログラムがデコードできないとき監査者は何をすべきか
何も言わないことです。ISO 14289-1 §7.21.4.2が要求する/CharSetと/CIDSetの完全性判定(診断10038と10039)は、意気込みすぎる検証器が負債に変わる場所です。読んだ人にとって「あなたのCharSetは不完全」という報告は「我々のType 1デコーダーが諦めた」と区別がつかないからです。そこでPDFlibPasは、三つのことがすべて成功したときにだけ欠落エントリを報告します。フォントプログラムがデコードされること、コードからグリフへのマッピングが解決されること、そしてセットそのものがデコードされることです。グリフ名を一つも/CharSet文字列に対して照合する前に、TPDFType1Decoder.LoadPFBFromStringがTrueを返しcharstring数を出す必要があります。/CIDSet経路は、一ビットも判定する前にストリームが展開され、グリフ数が正で返ってくることを要します。途中の例外はいずれも、欠陥ではなく「指摘なし」へ潰れます
これは見逃しへの意図的な傾斜であり、埋もれるより率直に述べる価値があります。壊れたCFFテーブル、未対応のType 1変種、グリフ範囲より短い/CIDSetは、いずれも診断ではなく沈黙を生みます。理由はこうです。PDF/UA監査は、ツールを作らなかった作者へ転送されます。誤った告発は見逃しより高くつきます。作者は適合ファイルが適合していることを証明するために一日を潰し、レポート全体への信頼を失います。Matterhorn Protocolは、機械が判定できるチェックと人間が担うべきチェックを分けるときに、同じ区別を別の形で設けており、そのFontsチェックポイント(31)がまさにここらです。より厳しい読みが必要なら、PDFlibPasを高速ゲートとして走らせ、専用の検証器をセカンドオピニオンにします。この組み合わせはPDF/AとPDF/UAのプリフライト手順で述べたものと同じです
ページの/Contentsはストリームではなくリスト
コンテンツストリーム監査で最も高くつく単一のミスは、/Contentsを一つのストリームとして扱うことです。ISO 32000-1 §7.7.3.3は、ページがストリームの配列を保持することを許し、部分間の空白込みで連結したものがページプログラムになります。プロデューサーは任意の場所で分割し、BTが一つのメンバーに座り、対応するETが次のメンバーに座ることがあります。コンテンツプロセッサーは状態を保持します。マーク付きコンテンツのネスト深度、最後のTfが選んだフォント、テキストオブジェクトのフラグです。Processは入口でその状態をリセットします。配列メンバーごとに一度呼べば、最初のメンバー以降のすべてのストリームは現行フォントなしで始まるため、完璧にタグ付けされたテキストが、タグもフォントもないノイズとして読まれます。PDFlibPasはまず連結し、一度だけ処理します
function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
I: Integer;
begin
Result := '';
Obj := DerefIndRef(FDoc, Obj);
if Obj is TPDFStream then
Result := TPDFStream(Obj).GetDecodedStream
else if Obj is TPDFArray then
for I := 0 to TPDFArray(Obj).Count - 1 do
Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;
// 連結全体に対して一度のProcess呼び出し、メンバーごとに一度ではない
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
どのフォームXObjectが実際に非構造と数えられるのか
ページが実際に呼び出すもの、マーク付きコンテンツの外側の呼び出し箇所から呼ばれるもの、そしてそのコンテンツ自身がテキストを示すものだけです。診断10040はISO 14289-1 §7.20を、オブジェクト番号ごとに三つの独立した事実を記録することで強制します。テキストを持つ、呼び出された、マーク付きコンテンツ内から呼び出された、であり、最初の二つの交差から三つ目を引いたものだけを報告します。近道はどちらも、出荷してしまえる形で間違っています。/Resources内のテキストを持つフォームをすべてフラグ立てすれば、誰も描かないテンプレートライブラリーが罰せられ、呼び出されたフォームをすべてフラグ立てすれば、テキストを持たずタグ付けも不要なベクターロゴが罰せられます。呼び出し箇所はリソース名ではなくオブジェクト番号で解決します。同じフォームは、ページごとに異なる名前で日常的に到達されるからです。姉妹診断の10041は§7.21.8のために同じ連結プログラムを歩み、テキストを示す各オペランドをスコープ内のフォントで解決し、.notdefに落ちるコードを数えます。これはテキストレンダリングモードにかかわらず禁止されています。走査画像の背後で使われる不可視モードも含めてです。生き残ったフォームをどう包むべきかは構造ツリーの問いであり、タグ付きPDF構造の構築の記事で扱います
FontDescriptorを一切持たないフォント
未埋め込みフォントはこの監査にとって正当な入力であり、エラー状態ではありません。したがって埋め込み判定より下のヘルパーはすべて、それを生き延びなければなりません。PDFlibPasは/FontDescriptorが見当たらないとき、またはFontFile、FontFile2、FontFile3を一切持たない記述子のときに診断10020を記録し(名前がStandard 14の一つなら診断10022、§7.21.4 NOTE 5はこれをあからさまに免除しません)、その後もファイルの残りを歩み続けます。これがレポートの要点です。作者が望むのは一走査ですべての指摘であり、一実行につき一指摘ではありません。だからこそ、幅、cmap、CharSet、CIDSetの各ヘルパーに渡される記述子参照はNilであり得て、それぞれが入口でそれを判定します。先の判定が監査を打ち切ったと仮定したりしません。修正が欠けているものの埋め込みであるなら、その手順は既存PDFへの欠落フォントの埋め込みのノートにあります
監査の実行
一回の呼び出しで、自分が生成したとは限らないファイルに対して行います。TPDFlib.CheckFileComplianceは適合テストセレクター(ISO 14289-1:2014のPDF/UA-1なら2)を受け取り、ゼロか、エントリが数値コード、コロン、読めるメッセージからなる文字列リストのハンドルを返します。ここで扱ったフォントとコンテンツストリームの指摘はその範囲の10020から10041を占め、00xxxのPDF/Aコードとは数値的に分けられているため、混在したログも読めます。Optionsに1を渡すと最初の指摘で短絡します。それはオーサリングツールよりもビルドゲートで欲しい挙動です。メモリー上でまだ開かれている文書には、GetPDFUADiagnosticsがディスクを経由する往復なしで同等の検査を実行します
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2がPDF/UA-1を選択、Options = 0はすべての指摘を報告
Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
if Issues = 0 then
WriteLn('delivery.pdf: PDF/UA-1 conformant')
else
begin
Count := PDF.GetStringListCount(Issues);
for I := 1 to Count do
WriteLn(' ', PDF.GetStringListItem(Issues, I)); // 例: 10037 CIDFontType2 ...
end;
end;
このどれも、マシン上の外部検証バイナリーを必要としません。そこが、毎回のビルドで走るチェックと、誰かが思い出したときに走るチェックの違いです。ここで述べたコンプライアンスと診断のAPIは標準のPDFlibPas Delphi PDF Libraryに同梱され、製品ページはPDF/A、PDF/X、PDF/Eのテストスイートと並べてPDF/UA-1の診断コード表を全編載せています