Technical Article

GeoPDF em Delphi: Coordenadas Geoespaciais com PDFlibPas

A maioria dos programadores pensa numa página PDF como uma folha de papel com texto e imagens. Um PDF georreferenciado é mais do que isso. Transporta informação suficiente para obter um ponto na página, medido em unidades de página normais, e reportar a latitude e longitude correspondentes no mundo real. Este único facto é o que transforma um PDF num contentor viável para um mapa topográfico, um plano de levantamento cadastral, uma planta de zona inundável ou qualquer exportação de SIG que precise de ser impressa e continuar a fazer sentido. A geometria está lá no ficheiro; a única questão é se o seu carregador a consegue ler.

A razão pela qual isto passa despercebido é que um GeoPDF abre e imprime exatamente como qualquer outro PDF. Nada na página renderizada indica que o mapa está associado a um sistema de coordenadas. O registo reside em dicionários anexados ao objeto da página, nunca desenhados, e um visualizador que os ignore apresenta o mapa da mesma forma. Para fazer qualquer trabalho espacial com o ficheiro (leitura de coordenadas de levantamentos, reprojeção ou sobreposição com outras camadas), terá de percorrer esses dicionários manualmente.

Existem dois padrões em circulação

Um leitor que pretenda processar ficheiros do mundo real tem de lidar com dois esquemas de georreferenciação, porque ambos circulam no mercado e um determinado ficheiro pode usar qualquer um deles. O mais antigo é a codificação OGC descrita em OGC 08-139r2, que anexa um dicionário LGIDict (um dicionário de registo geoespacial) à página. Este modelo antecede qualquer aprovação da ISO e foi o formato de facto para os primeiros resultados de GeoPDF, pelo que uma grande quantidade de mapas antigos o inclui exclusivamente.

O esquema moderno é o que foi normalizado pela ISO na norma ISO 32000-1 §8.8.2. Em vez de um único dicionário ao nível da página, ele modela dados geoespaciais como uma Viewport de página com um dicionário Measure anexado, e esse dicionário de medição identifica um sistema de coordenadas geográficas. Esta é a codificação gerada pelo Acrobat e pelos exportadores de SIG atuais. Um importador robusto verifica ambos: lê as viewports para o modelo ISO e recorre ao (ou também inspeciona o) LGIDict em ficheiros que apenas contêm o registo antigo.

Viewports e os seus limites

No modelo ISO, a unidade de georreferenciação é a viewport, e uma página pode conter várias. Uma folha de grande dimensão pode colocar um mapa principal num retângulo, um mapa de pormenor (inset) noutra escala num segundo retângulo, e uma legenda que não esteja georreferenciada. Cada viewport contém uma BBox, o retângulo na página governado pela viewport, para que o leitor saiba a que parte da folha se aplica determinado sistema de coordenadas. Verificar o impacto de um ponto clicado contra essas caixas (hit-testing) é como o visualizador decide qual o dicionário de medição a utilizar.

O PDFlibPas expõe as viewports da página selecionada diretamente. GetPageViewPortCount devolve quantas existem, GetPageViewPortID converte um índice baseado em um num handle de ViewPortID, e GetViewPortBBox lê o retângulo delimitador uma dimensão de cada vez. O argumento Dimension seleciona a aresta ou extensão pretendida: 0 é Left, 1 is Top, 2 is Width, 3 is Height, 4 is Right e 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;

Uma ViewPortID com valor zero proveniente de GetPageViewPortID significa que a viewport nesse índice não pôde ser encontrada, pelo que deve validá-la antes de partilhar o handle.

Dentro do dicionário de medição

A geometria que associa a página ao mundo real reside no dicionário de medição (measure dictionary) anexado a uma viewport. O método GetViewPortMeasureDict retorna um MeasureDictID para uma determinada ViewPortID, ou zero caso a viewport não contenha um dicionário de medição, o que constitui o cenário normal para uma legenda ou painel de título. O dicionário de medição armazena três elementos que vale a pena ler: os sistemas de coordenadas a que faz referência, os arrays que associam pontos de página a pontos geográficos, e a unidade na qual os dados dos pontos são expressos.

O registo em si consiste em dois arrays paralelos. GPTS é o array de pontos geográficos, isto é, pares de latitude e longitude fornecidos no sistema de coordenadas geográficas. LPTS é o array de pontos no espaço de página, representados como frações da BBox da viewport para que sobrevivam a redimensionamentos. O item n de LPTS e o item n de GPTS indicam a mesma localização física, uma vez em coordenadas de página e outra no globo. Três ou mais pares deste tipo determinam a transformação afim (ou projetiva, no caso geral) que mapeia qualquer coordenada de página dentro da viewport para uma coordenada mundial. Lê-los implica percorrer ambos os arrays em simultâneo.

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;

O dicionário de medição também reporta as suas unidades de exibição através de GetMeasureDictPDU, que recebe um UnitIndex de 1 para linear, 2 para área ou 3 para angular, devolvendo um código que identifica a unidade específica, por exemplo, o metro ou o pé internacional na categoria linear. O array Bounds, lido com GetMeasureDictBoundsItem, descreve o quadrilátero dentro da viewport efetivamente abrangido pela medição, o qual nem sempre corresponde ao retângulo completo.

Contexto prático

A latitude e longitude em GPTS não têm significado sem conhecer o sistema de coordenadas geográficas a que pertencem, dado que uma coordenada de 51.5, -0.1 aterra num local físico diferente sob o WGS 84 do que sob um referencial nacional antigo. O dicionário de medição esclarece isto através de um dicionário de sistema de coordenadas, acedido com GetMeasureDictGCSDict para o sistema geográfico. O PDF descreve esse sistema de uma de duas formas intermutáveis, cabendo ao leitor aceitar qualquer uma delas.

A primeira é WKT (Well-Known Text), uma string autónoma que especifica por extenso o datum, elipsoide, meridiano de origem e unidades. É detalhada mas inequívoca, dispensando tabelas de consulta externas. A segunda é um código EPSG, um inteiro simples que aponta para um sistema de coordenadas no registo EPSG; o valor 4326 representa o WGS 84, o referencial utilizado pela maioria dos dados de GPS de consumo. O EPSG é compacto, mas pressupõe que o leitor consiga decodificar o código contra uma base de dados. Os ficheiros apresentam-se com uma, outra ou ambas as opções, razão pela qual a API disponibiliza GetCSDictType, GetCSDictEPSG e GetCSDictWKT. O GetCSDictType reporta se o sistema é geográfico (um GEOGCS, retorno com valor 1) ou projetado (um PROJCS, retorno com valor 2), permitindo interpretar o restante de forma correta antes de validar a informação.

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;

Ler o dicionário antigo LGIDict

Os ficheiros anteriores ao modelo de viewports, ou que tenham sido gerados por ferramentas que ainda emitem a codificação antiga, transportam a sua georreferenciação num LGIDict na página, em vez de num dicionário de medição. O PDFlibPas reporta quantos dicionários deste tipo uma página contém através de GetPageLGIDictCount e devolve o conteúdo em bruto de cada um com GetPageLGIDictContent, indexado a partir de um. O texto retornado é o dicionário conforme escrito, contendo os campos de registo da OGC 08-139r2, os quais o seu código depois analisa para recuperar o mesmo tipo de mapeamento página-mundo oferecido pelo dicionário de medição. No lado da escrita, AddLGIDictToPage associa um LGIDict à página atual, para que um conversor possa processar o formato legado se um destinatário antigo ainda o exigir.

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;

Consolidar a leitura

Um importador completo trata os dois esquemas como um par de passagens sobre cada página. Selecione a página, solicite o número de viewports ISO com GetPageViewPortCount e, para cada viewport com um dicionário de medição, extraia a sua BBox, os arrays GPTS e LPTS, a unidade de dados dos pontos e a descrição do GCS através do dicionário do sistema de coordenadas. De seguida, consulte GetPageLGIDictCount para detetar qualquer registo legado que a passagem das viewports não tenha contemplado. Um mapa que inclua ambas as opções deve apresentar concordância entre as duas; um mapa com apenas uma opção continua a ser descodificado, porque inspecionou ambos os locais. Os handles retornados no processo, como ViewPortID, MeasureDictID e CSDictID, são inteiros simples que permanecem válidos enquanto o documento estiver carregado, pelo que toda a análise se resume a alguns ciclos aninhados sobre a lista de páginas, sem alocações para gerir.

Once you can recover the registration, the page becomes a data source rather than a picture. The companion techniques for reading the rest of a page are covered in the article on text, image, and font extraction, and rendering a georeferenced sheet to a device for on-screen measurement is described in the print and preview device-context walkthrough. The geospatial reader described here ships as part of the losLab PDF Library for Delphi and C++Builder, alongside the loading, extraction, and rendering APIs covered elsewhere on this blog.