Technical Article

GeoPDF em Delphi: Coordenadas geoespaciais com PDFlibPas

A maioria dos desenvolvedores pensa em uma página PDF como uma folha de papel com textos e imagens. Um PDF georreferenciado é mais do que isso. Ele carrega informações suficientes para pegar um ponto na página, medido em unidades normais de página, e informar a latitude e longitude correspondentes no mundo real. Esse único fato é o que transforma um PDF em um meio utilizável para um mapa topográfico, um loteamento cadastral, um estudo de zona de inundação ou qualquer exportação de SIG que precise ser impressa e ainda ter significado geográfico. A geometria está presente no arquivo; a única questão é se o seu carregador a lê.

O motivo pelo qual isso passa despercebido é que um GeoPDF abre e imprime exatamente como qualquer outro PDF. Nada na página renderizada anuncia que o mapa está registrado em um sistema de coordenadas. O registro reside em dicionários vinculados ao objeto da página, nunca desenhados, e um visualizador que os ignora exibe o mapa da mesma forma. Para fazer qualquer trabalho espacial com o arquivo, como leituras de coordenadas de agrimensura, reprojeção ou sobreposição com outras camadas, você mesmo precisa percorrer esses dicionários.

Dois padrões coexistem no mercado

Um leitor que deseja processar arquivos do mundo real precisa lidar com dois esquemas de georreferenciamento, porque ambos estão em circulação e um determinado arquivo pode usar qualquer um deles. O mais antigo é a codificação OGC descrita na norma OGC 08-139r2, que associa um LGIDict (um dicionário de registro geoespacial) à página. Ele precede qualquer homologação da ISO e foi o formato padrão das primeiras saídas GeoPDF, de modo que um grande volume de mapas antigos o carrega e nada mais.

O esquema moderno é o que a ISO padronizou na ISO 32000-1 §8.8.2. Em vez de um único dicionário no nível da página, ele modela dados geoespaciais como uma página Viewport com um dicionário Measure associado, e o dicionário de medição nomeia um sistema de coordenadas geográficas. Essa é a codificação que o Acrobat e os exportadores atuais de SIG gravam. Um importador robusto verifica ambos: lê as viewports para o modelo ISO e recorre (ou inspeciona adicionalmente) ao LGIDict para arquivos que carregam apenas o registro antigo.

Viewports e seus limites

No modelo ISO, a unidade de georreferenciamento é a viewport, e uma página pode conter várias. Uma folha grande pode colocar um mapa principal em um retângulo, um detalhe em escala diferente em outro e um painel de legenda sem qualquer georreferenciamento. Cada viewport carrega um BBox, o retângulo na página que a viewport governa, para que o leitor saiba a qual parte da folha um determinado sistema de coordenadas se aplica. Testar a interseção de um ponto clicado com esses retângulos é a maneira pela qual um visualizador decide qual dicionário de medição usar.

O PDFlibPas expõe as viewports da página selecionada diretamente. O GetPageViewPortCount retorna quantas viewports existem, o GetPageViewPortID converte um índice baseado em um em um handle ViewPortID, e o GetViewPortBBox lê o retângulo delimitador uma dimensão de cada vez. O argumento Dimension seleciona qual borda ou extensão você deseja: 0 é Esquerda (Left), 1 é Topo (Top), 2 é Largura (Width), 3 é Altura (Height), 4 é Direita (Right) e 5 é Fundo (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;

Um ViewPortID igual a zero retornado por GetPageViewPortID significa que a viewport nesse índice não pôde ser encontrada, logo faça essa verificação antes de repassar o handle.

Por dentro do dicionário de medição

A geometria que registra a página com o mundo reside no dicionário de medição associado a uma viewport. O GetViewPortMeasureDict retorna um MeasureDictID para um determinado ViewPortID, ou zero quando a viewport não possui dicionário de medição, o que é o caso normal para uma legenda ou painel de título. O dicionário de medição contém três coisas importantes a serem lidas: os sistemas de coordenadas aos quais faz referência, os arrays que vinculam pontos da página a pontos geográficos e a unidade na qual os dados dos pontos são expressos.

O registro em si consiste em dois arrays paralelos. O GPTS é o array de pontos geográficos, pares de latitude e longitude fornecidos no sistema de coordenadas geográficas. O LPTS é o array de pontos no espaço da página, expressos como frações do BBox da viewport para que sobrevivam ao redimensionamento. O item n do LPTS e o item n do GPTS nomeiam a mesma localização física, uma vez em coordenadas de página e outra no globo. Três ou mais pares desse tipo estabelecem 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 é uma questão de percorrer ambos os arrays em sincronia.

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 informa suas unidades de exibição por meio de GetMeasureDictPDU, que recebe um UnitIndex de 1 para unidades lineares, 2 para áreas ou 3 para unidades angulares e retorna um código identificando a unidade específica, por exemplo, um metro ou um pé internacional para a categoria linear. O array Bounds, lido com GetMeasureDictBoundsItem, descreve o quadrilátero dentro da viewport que a medição realmente cobre, o que nem sempre corresponde ao retângulo completo.

WKT versus EPSG

A latitude e longitude em GPTS não têm significado sem conhecer a qual sistema de coordenadas geográficas pertencem, já que uma coordenada de 51.5, -0.1 cai em um ponto físico diferente sob o WGS 84 em comparação com um datum nacional mais antigo. O dicionário de medição responde a isso por meio de um dicionário de sistema de coordenadas, acessado com GetMeasureDictGCSDict para o sistema geográfico. O PDF descreve esse sistema de uma de duas maneiras intercambiáveis, e um leitor precisa aceitar ambas.

A primeira é WKT (Well-Known Text), uma string independente que detalha o datum, elipsoide, meridiano de origem e unidades por completo. É detalhada, mas inequívoca e dispensa tabela de consulta externa. A segunda é um código EPSG, um único inteiro que indexa um sistema de coordenadas no registro EPSG; 4326 corresponde ao WGS 84, o referencial que a maioria dos dados de GPS de consumo utiliza. O EPSG é compacto, mas pressupõe que o leitor consiga correlacionar o código com um banco de dados. Os arquivos aparecem com um, o outro ou ambos, razão pela qual a API disponibiliza os três métodos: GetCSDictType, GetCSDictEPSG e GetCSDictWKT. O GetCSDictType relata se o sistema é geográfico (um GEOGCS, valor de retorno 1) ou projetado (um PROJCS, valor de retorno 2), permitindo interpretar o restante corretamente antes de confiar nele.

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;

Lendo o LGIDict antigo

Arquivos anteriores ao modelo de viewport, ou produzidos por ferramentas que ainda geram a codificação antiga, trazem seu registro em um LGIDict na página, em vez de um dicionário de medição. O PDFlibPas informa quantos dicionários desse tipo uma página possui por meio de GetPageLGIDictCount e entrega o conteúdo bruto de cada um com GetPageLGIDictContent, indexado a partir de um. O texto retornado é o dicionário conforme gravado, contendo os campos de registro da norma OGC 08-139r2, os quais o seu código analisa para recuperar o mesmo tipo de mapeamento de página para mundo que o dicionário de medição fornece. No lado da gravação, o AddLGIDictToPage anexa um LGIDict à página atual, permitindo que um conversor processe o formato antigo quando um leitor legado 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;

Unindo as leituras

Um importador completo trata os dois esquemas como um par de etapas sobre cada página. Selecione a página, solicite ao GetPageViewPortCount as viewports ISO e, para cada viewport que possua um dicionário de medição, extraia seu BBox, seus arrays GPTS e LPTS, sua unidade de dados de pontos e a descrição de GCS por meio do dicionário do sistema de coordenadas. Em seguida, verifique no GetPageLGIDictCount qualquer registro antigo que a verificação de viewports não tenha coberto. Um mapa que carrega ambos os esquemas deve apresentar concordância entre eles; um mapa que carrega apenas um ainda resolve, porque você buscou em ambos os locais. Os handles retornados ao longo do caminho (ViewPortID, MeasureDictID, CSDictID) são inteiros simples que permanecem válidos enquanto o documento está carregado, de modo que toda a leitura consiste em alguns loops aninhados sobre a lista de páginas sem alocação para gerenciar.

Uma vez que você consegue recuperar o registro, a página se torna uma fonte de dados em vez de uma simples imagem. As técnicas complementares para ler o restante de uma página são cobertas em nosso artigo sobre extração de texto, imagem e fonte, e a renderização de uma folha georreferenciada em um dispositivo para medições na tela é descrita no tutorial sobre contexto de dispositivo para impressão e visualização. O leitor geoespacial descrito aqui é fornecido como parte da losLab PDF Library para Delphi e C++Builder, junto com as APIs de carregamento, extração e renderização abordadas em outras seções deste blog.