Technical Article

Delphi 中的 GeoPDF:使用 PDFlibPas 處理地理空間座標

大多數開發人員將 PDF 頁面視為一張帶有文字和圖片的紙張;地理參考(Georeferenced)PDF 不僅如此;它攜帶了足夠的資訊,可以獲取頁面上的一點(以一般頁面單位測量),並回報它在真實世界中坐落的緯度和經度;這單一事實使 PDF 成為地形圖、地籍測量圖、防汛區展示或任何必須列印且仍具有意義之 GIS 匯出內容的可用載體;幾何結構存在於檔案中,唯一的問題是您的載入器是否讀取它

此點被忽略的原因是 GeoPDF 的開啟和列印方式與其他任何 PDF 完全相同;轉譯後的頁面中沒有任何內容宣告地圖已註冊到座標系統;註冊資訊存在於懸掛在頁面物件上的字典中,從不被繪製,而忽略它們的檢視器仍會顯示地圖;要對檔案進行任何空間操作(測量座標讀數、重投影、與其他圖層疊加),您必須自己遍歷這些字典

野外存在兩種標準

傳統的 georegistration 讀取器必須應對兩種地理註冊配置,因為兩者都在流通中,且指定的檔案可能使用其中任何一種;較舊的一種是 OGC 08-139r2 中描述的 OGC 編碼,它將 LGIDict(地理空間註冊字典)附加到頁面;它早於任何 ISO 認可,並且是早期 GeoPDF 輸出的事實上(De facto)格式,因此大量遺留地圖僅攜帶它

現代配置是 ISO 在 ISO 32000-1 §8.8.2 中標準化的配置;它沒有使用單一頁面級字典,而是將地理空間資料模型化為帶有附加測量(Measure)字典的頁面視埠(Viewport),且測量字典命名了一個地理座標系統;這是 Acrobat 和目前 GIS 匯出器所寫入的編碼;強固的匯入器會對兩者進行檢查:讀取 ISO 模型的視埠,並針對僅攜帶遺留註冊的檔案回復到(或另外檢查)LGIDict

視埠及其邊界

在 ISO 模型中,地理註冊的單位是視埠,且一個頁面可能有多個視埠;一張大紙張可以將主地圖放在一個矩形中,將不同比例的插圖放在另一個矩形中,以及放一個完全沒有地理參考的圖例面板;每個視埠都攜帶一個 BBox(即頁面上由視埠控制的矩形),因此讀取器知道指定的座標系統適用於紙張的哪個部分;對按一下的點與這些方框進行碰撞測試(Hit-testing),是檢視器決定使用哪個測量字典的方式

PDFlibPas 直接公開選定頁面的視埠;GetPageViewPortCount 傳回有多少個視埠, GetPageViewPortID 將從 1 開始的索引轉換為 ViewPortID 控制代碼,而 GetViewPortBBox 一次讀取一個維度的邊界矩形;Dimension 引數選擇您想要的邊緣或範圍:0 是 Left,1 是 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,或者當視埠沒有測量字典時傳回零(這是圖例或標題面板的一般情況);測量字典保存了三件值得讀取的內容:它所參照的座標系統、將頁面點連結到地理點的陣列,以及表示點資料的單位

註冊本身是兩個平行的陣列;GPTS 是地理點的陣列,即地理座標系統中給出的緯度和經度對;LPTS 是頁面空間點的陣列,表示為視埠 BBox 的分數,以便它們在縮放後存活;LPTS 的項目 n 和 GPTS 的項目 n 命名相同的物理位置,一次在頁面座標中,一次在地球上;三個或更多此類對可以固定仿射(Affine)轉換,或者在一般情況下的投影(Projective)轉換,該轉換將視埠內部的任何頁面座標對應到世界座標;讀取研們就是步調一致地遍歷這兩個陣列

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 回報其顯示單位,該方法獲取線性單位之 UnitIndex 1、面積單位之 2,或角度單位之 3,並傳回識別特定單元的程式碼(例如線性類別的公尺或國際英呎);透過 GetMeasureDictBoundsItem 讀取的 Bounds 陣列描述了視埠內測量實際涵蓋的四邊形,它並不總是完整的矩形

WKT 與 EPSG

在不知道緯度和經度屬於哪個地理座標系統的情況下,GPTS 中的經緯度是沒有意義的,因為在 WGS 84 下,51.5, -0.1 的座標與在較舊的國家基準面下所對應的物理位置不同;測量字典透過座標系統字典回答了這個問題(透過針對地理系統的 GetMeasureDictGCSDict 連入);PDF 以兩種可互換方式之一描述該系統,且讀取器必須接受其中任何一種

第一種是 WKT(Well-Known Text),這是一個獨立的字串,完整拼寫出基準面、橢球體、本初子午線和單位;它很冗長但明確,且不需要外部對照表;第二種是 EPSG 程式碼(一個在 EPSG 註冊表中索引座標系統的單個整數);4326 是 WGS 84(大多數消費級 GPS 資料使用的框架);EPSG 很緊湊,但假設讀取器可以針對資料庫解析程式碼;檔案會帶有其中之一、另一種或兩者,這就是為什麼 API 公開了 GetCSDictTypeGetCSDictEPSGGetCSDictWKT 這三者的原因;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;

將讀取結合在一起

完整的匯入器將這兩種配置視為對每個頁面的一對處理程序;選擇頁面,向 GetPageViewPortCount 要求 ISO 視埠,且對於擁有測量字典的每個視埠,透過座標系統字典提取其 BBox、其 GPTS 和 LPTS 陣列、其點資料單位以及 GCS 描述;然後檢查 GetPageLGIDictCount 以獲取視埠處理程序未涵蓋的任何遺留註冊;攜帶兩者的地圖應該在它們之間保持一致;僅攜帶其中之一的地圖仍然可以解析,因為您在兩個地方都進行了尋找;沿途傳回的控制代碼(ViewPortID、MeasureDictID、CSDictID)是簡單的整數,在檔案載入時保持有效,因此整個遍歷是頁面清單上的幾個巢狀迴圈,不需要管理配置

一旦您可以復原註冊資訊,頁面就會變為資料來源端而不是圖片;讀取頁面其餘內容的配套技術在文字、影像和字型提取的文章中有所介紹,而將具有地理參考的紙張轉譯到裝置以進行螢幕測量則在列印和預覽裝置內容逐步說明中有所描述;此處所述的地理空間讀取器作為 Delphi 和 C++Builder 的 losLab PDF Library 的一部分隨附,並與本部落格其他地方介紹的載入、提取和轉譯 API 搭配