Technical Article

DelphiにおけるGeoPDF:PDFlibPasによる地理空間座標の処理

多くの開発者は、PDFページをテキストや画像が配置された単なる紙のシートと考えています。しかし、地理参照された(georeferenced)PDFはそれ以上のものです。通常のページ単位で測定されたページ上の特定のポイントから、そのポイントが現実世界のどこの緯度・経度に対応しているかを報告するのに十分な情報を含んでいます。この事実こそが、PDFを地形図、地籍測量図、洪水危険地帯図、あるいは印刷されつつも価値を持つ必要のあるあらゆるGISエクスポートデータを格納する有用なコンテナに変えるのです。必要な幾何データはファイル内に存在します。問題は、ローダーがそれを読み取るかどうかだけです。

これが無視されがちな理由は、GeoPDFが他のあらゆるPDFと全く同様に表示および印刷されるためです。レンダリングされたページには、その地図が座標系に登録されていることを示す表示は一切ありません。位置登録データは、描画されることのないページオブジェクトに付随するディクショナリ内に存在し、それらを無視するビューアでも地図自体は同様に表示されます。ファイルを使用して空間処理(測量座標の読み出し、再投影、他のレイヤとの重ね合わせなど)を行うには、自身でそれらのディクショナリをたどる必要があります。

実用化されている2つの標準規格

現実世界のファイルを処理するリーダーは、2つの地理登録スキーマに対応できなければなりません。両方が流通しており、特定のファイルがいずれかを使用している可能性があるためです。古い方は、ページにLGIDict(地理空間登録ディクショナリ)を添付する、OGC 08-139r2に記述されているOGCエンコーディングです。これはISOによる承認よりも前に作られたもので、初期のGeoPDF出力の事実上の標準形式であったため、多くのレガシーな地図にはこれだけが含まれています。

現代的なスキーマは、ISOがISO 32000-1 §8.8.2で標準化したものです。単一のページレベルのディクショナリの代わりに、地理空間データを、計測(Measure)ディクショナリが添付されたページビューポート(Viewport)としてモデル化し、計測ディクショナリが地理座標系を指定します。これが、Acrobatや現在のGISエクスポートツールが書き出すエンコーディングです。印刷用インポーターは両方をチェックします。すなわち、ISOモデル用にビューポートを読み込み、従来の登録データしか持たないファイルに対してはLGIDictにフォールバック(または追加で検査)します。

ビューポートとその境界

ISOモデルでは、地理登録の単位はビューポートであり、1ページに複数存在することがあります。大きな用紙の場合、メインの地図を1つの長方形に配置し、縮尺の異なるインセット地図を別の長方形に配置し、地理参照されていない凡例パネルを配置することができます。各ビューポートは、そのビューポートが支配するページ上の長方形領域であるBBoxを保持しているため、リーダーはドキュメントのどの部分に特定の座標系が適用されるかを把握できます。クリックされたポイントをこれらのボックスに対してヒットテストすることが、ビューアがどの計測ディクショナリを使用するかを決定する方法です。

PDFlibPasは、選択されたページのビューポートを直接公開しています。GetPageViewPortCountはビューポートの数を返し、GetPageViewPortIDは1ベースのインデックスをViewPortIDハンドルに変換し、GetViewPortBBoxは境界の長方形を1次元ずつ読み取ります。Dimension引数は、必要な境界線または範囲を選択します。0はLeft、1 is Top、2はWidth、3はHeight、4はRight、5はBottomです。

var
  Pdf: TPDFlib;
  vpCount, i, vpID: Integer;
  Left, Top, Width, Height: Double;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('topo_sheet.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);

    vpCount := Pdf.GetPageViewPortCount;
    for i := 1 to vpCount do
    begin
      vpID := Pdf.GetPageViewPortID(i);
      Left   := Pdf.GetViewPortBBox(vpID, 0);
      Top    := Pdf.GetViewPortBBox(vpID, 1);
      Width  := Pdf.GetViewPortBBox(vpID, 2);
      Height := Pdf.GetViewPortBBox(vpID, 3);
      // Left/Top/Width/Height describe the map area for this viewport
    end;
  finally
    Pdf.Free;
  end;
end;

GetPageViewPortIDから返されるViewPortIDがゼロである場合は、そのインデックスのビューポートが見つからなかったことを意味するため、ハンドルを渡す前にチェックしてください。

計測ディクショナリの内部

ページと現実世界を位置登録する幾何データは、ビューポートに添付された計測ディクショナリに格納されています。GetViewPortMeasureDictは指定されたViewPortIDに対するMeasureDictIDを返します。ビューポートに計測ディクショナリがない場合(凡例やタイトルパネルなどの通常のケース)はゼロを返します。計測ディクショナリには、読み取る価値のある3つのデータが保持されています。参照している座標系、ページ上の点と地理的な点を結びつける配列、および点のデータが表現される単位です。

位置登録そのものは、2つの並行する配列で行われます。GPTSは地理座標系で示される緯度と経度のペアである地理座標点の配列です。LPTSは、スケーリングに対応できるようにビューポートのBBoxの割合として表現されたページ空間座標点の配列です。LPTSの要素nとGPTSの要素nは、同じ物理的な位置を、一度はページ座標で、もう一度は地球上の座標で表します。3つ以上のペアを使用すれば、ビューポート内の任意のページ座標を現実世界の座標にマッピングするアフィン変換(または一般的な射影変換)を確定できます。これらを読み取るには、両方の配列を同期して処理します。

var
  measID, gptsCount, lptsCount, j: Integer;
  lat, lon, px, py: Double;
begin
  measID := Pdf.GetViewPortMeasureDict(vpID);
  if measID <> 0 then
  begin
    gptsCount := Pdf.GetMeasureDictGPTSCount(measID);
    lptsCount := Pdf.GetMeasureDictLPTSCount(measID);
    // GPTS holds lat/lon pairs; LPTS holds the matching page fractions.
    // Both arrays are read with one-based item indices.
    j := 1;
    while j < gptsCount do
    begin
      lat := Pdf.GetMeasureDictGPTSItem(measID, j);
      lon := Pdf.GetMeasureDictGPTSItem(measID, j + 1);
      px  := Pdf.GetMeasureDictLPTSItem(measID, j);
      py  := Pdf.GetMeasureDictLPTSItem(measID, j + 1);
      // (px, py) on the page corresponds to (lat, lon) on the ground
      Inc(j, 2);
    end;
  end;
end;

計測ディクショナリは、GetMeasureDictPDUを介して表示単位も報告します。これは、距離を示す1、面積を示す2、または角度を示す3のUnitIndexを受け取り、特定の単位(距離カテゴリの場合はメートルや国際フィートなど)を示すコードを返します。GetMeasureDictBoundsItemで読み取られるBounds配列は、ビューポート内において実際に計測がカバーする四角形領域を記述しますが、これは必ずしも常に長方形全体とは限りません。

WKTとEPSG

GPTS内の緯度と経度は、それらがどの地理座標系に属しているかを知らなければ意味を持ちません。51.5, -0.1という座標は、古い国内測地系とWGS 84とでは異なる物理的な位置を指し示すからです。計測ディクショナリは、地理座標系のためにGetMeasureDictGCSDictでアクセスされる座標系ディクショナリを介してこれに答えます。PDFは、この座標系を互換性のある2つの方法のいずれかで記述しており、リーダーはそのどちらをも受け入れる必要があります。

1つ目はWKT(Well-Known Text)で、データム(測地系)、楕円体、本初子午線、および単位を詳細に記述した独立した文字列です。冗長ですが曖昧さがなく、外部の参照テーブルを必要としません。2つ目はEPSGコードで、EPSGレジストリ内の座標系をインデックス化する単一の整数です。例えば、一般のGPSデータで最もよく使われるWGS 84は4326です。EPSGはコンパクトですが、リーダーがそのコードをデータベースと照合して解決できることを前提としています。ファイルはいずれか一方、あるいは両方を含んで存在するため、APIはGetCSDictTypeGetCSDictEPSG、およびGetCSDictWKTのすべてを公開しています。GetCSDictTypeは座標系が地理座標系(GEOGCS、戻り値1)であるか、投影座標系(PROJCS、戻り値2)であるかを報告し、信頼する前に残りのデータを正しく解釈できるようにします。

var
  gcsID, csType, epsg: Integer;
  wkt: WideString;
begin
  gcsID := Pdf.GetMeasureDictGCSDict(measID);
  if gcsID <> 0 then
  begin
    csType := Pdf.GetCSDictType(gcsID);   // 1 = GEOGCS, 2 = PROJCS
    epsg   := Pdf.GetCSDictEPSG(gcsID);   // e.g. 4326 for WGS 84, 0 if absent
    wkt    := Pdf.GetCSDictWKT(gcsID);    // full text description, '' if absent
    // Prefer EPSG when present; fall back to parsing WKT otherwise.
  end;
end;

レガシーなLGIDictの読み込み

ビューポートモデルよりも古いファイルや、古いエンコーディングを出力し続けているツールで作成されたファイルは、位置登録データを計測ディクショナリではなく、ページ上のLGIDictに保持しています。PDFlibPasは、GetPageLGIDictCountを介してページが保持するそのディクショナリの数を報告し、1ベースでインデックス化されたそれぞれの生のコンテンツをGetPageLGIDictContentで返します。返されるテキストは記述された通りのディクショナリであり、OGC 08-139r2の位置登録フィールドを保持しており、コードはそれを解析して計測ディクショナリが提供するのと同様のページ対現実世界のマッピングを復元します。書き込み側では、AddLGIDictToPageが現在のページにLGIDictを添付するため、古いコンシューマーが依然としてレガシー形式を期待している場合に、コンバータでレガシー形式をラウンドトリップさせることができます。

var
  lgiCount, k: Integer;
  dictText: WideString;
begin
  lgiCount := Pdf.GetPageLGIDictCount;
  for k := 1 to lgiCount do
  begin
    dictText := Pdf.GetPageLGIDictContent(k);
    // dictText carries the OGC 08-139r2 registration to parse
  end;
end;

読み込み処理の統合

完全なインポーターは、2つのスキーマを各ページに対する一対のパスとして扱います。ページを選択し、GetPageViewPortCountでISOビューポートを取得し、計測ディクショナリを所有するすべてのビューポートについて、BBox、GPTSおよびLPTS配列、点のデータ単位、および座標系ディクショナリを介したGCS記述を取得します。その後、ビューポートパスでカバーされなかったレガシー登録データがないかGetPageLGIDictCountを確認します。両方を保持する地図ではそれらのデータが一致している必要がありますが、一方のみを保持する地図であっても、両方の場所を確認するため正しく解決できます。処理の過程で返されるハンドル(ViewPortID、MeasureDictID、CSDictID)は単なる整数であり、ドキュメントがロードされている間は有効に維持されるため、プロセス全体はページリストを走査するいくつかのネストしたループになり、管理すべきメモリ割り当てはありません。

位置登録データを復元できるようになれば、ページは単なる画像ではなくデータソースになります。ページの残りの部分を読み取る関連手法は、テキスト、画像、およびフォント抽出に関する記事でカバーされており、画面上での測定のために地理参照されたシートをデバイスにレンダリングする方法は、印刷とプレビューのデバイスコンテキストチュートリアルで説明されています。ここで解説した地理空間リーダーは、このブログの他の場所で扱われているロード、抽出、およびレンダリングAPIと並んで、DelphiおよびC++Builder向けのlosLab PDF Libraryの一部として提供されています。