Technical Article

GeoPDF en Delphi: coordenadas geoespaciales con PDFlibPas

La mayoría de los desarrolladores piensan en una página PDF como una hoja de papel con texto e imágenes. Un PDF georreferenciado es más que eso. Lleva suficiente información para tomar un punto en la página, medido en unidades de página ordinarias, y reportar la latitud y la longitud sobre las que se asienta en el mundo real. Ese único hecho es lo que convierte a un PDF en un portador útil para un mapa topográfico, un plano de levantamiento catastral, un anexo de zona de inundación o cualquier exportación de SIG que tenga que imprimirse y seguir significando algo. La geometría está ahí en el archivo; la única pregunta es si su cargador la lee

La razón por la que esto se pasa por alto es que un GeoPDF se abre e imprime exactamente como cualquier otro PDF. Nada en la página procesada anuncia que el mapa está registrado en un sistema de coordenadas. El registro reside en diccionarios que cuelgan del objeto de página, nunca dibujados, y un visor que los ignore le mostrará el mapa de todos modos. Para hacer cualquier cosa espacial con el archivo, lecturas de coordenadas de topografía, reproyección o superposición frente a otras capas, tiene que recorrer esos diccionarios usted mismo

Dos estándares coexisten en la práctica

Un lector que quiera manejar archivos del mundo real tiene que hacer frente a dos esquemas de georregistro, porque ambos están en circulación y un archivo determinado puede utilizar cualquiera de ellos. El más antiguo es la codificación OGC descrita en OGC 08-139r2, que adjunta un LGIDict (un diccionario de registro geoespacial) a la página. Es anterior a cualquier aprobación de la ISO y fue el formato de facto para las primeras salidas de GeoPDF, por lo que un gran conjunto de mapas heredados lo lleva y nada más

El esquema moderno es el que la ISO estandarizó en ISO 32000-1 §8.8.2. En lugar de un único diccionario a nivel de página, modela los datos geoespaciales como una ventana gráfica (Viewport) de página con un diccionario de medición (Measure) adjunto, y el diccionario de medición nombra un sistema de coordenadas geográficas. Esta es la codificación que escriben Acrobat y los exportadores de SIG actuales. Un importador robusto comprueba ambos: lee las ventanas gráficas para el modelo ISO y recurre a (o inspecciona adicionalmente) el LGIDict para archivos que solo llevan el registro heredado

Ventanas gráficas y sus límites

En el documento ISO, la unidad de georregistro es la ventana gráfica, y una página puede tener varias. Una hoja grande puede colocar un mapa principal en un rectángulo, un recuadro a una escala diferente en otro y un panel de leyenda que no esté georreferenciado en absoluto. Cada ventana gráfica lleva un BBox, el rectángulo en la página que gobierna la ventana gráfica, de modo que el lector sepa qué parte de la hoja se aplica a un sistema de coordenadas dado. Realizar pruebas de impacto de un punto en el que se hizo clic frente a esos cuadros es la forma en que un visor decide qué diccionario de medición utilizar

PDFlibPas expone directamente las ventanas gráficas de la página seleccionada. GetPageViewPortCount devuelve cuántas hay, GetPageViewPortID convierte un índice basado en uno en un controlador de ViewPortID, y GetViewPortBBox lee el rectángulo delimitador una dimensión a la vez. El argumento Dimension selecciona qué borde o extensión desea: 0 es Left, 1 es Top, 2 es Width, 3 es Height, 4 es Right y 5 es 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;

Un ViewPortID de cero de GetPageViewPortID significa que la ventana gráfica en ese índice no se pudo encontrar, así que compruébelo antes de pasar el controlador

Dentro del diccionario de medición

La geometría que registra la página con el mundo reside en el diccionario de medición adjunto a una ventana gráfica. GetViewPortMeasureDict devuelve un MeasureDictID para un ViewPortID dado, o cero cuando la ventana gráfica no tiene diccionario de medición, que es el caso normal para un panel de leyenda o de título. El diccionario de medición contiene tres cosas que vale la pena leer: los sistemas de coordenadas que referencia, las matrices que vinculan los puntos de la página con los puntos geográficos y la unidad en la que se expresan los datos de los puntos

El registro en sí son dos matrices paralelas. GPTS es la matriz de puntos geográficos, pares de latitud y longitud dados en el sistema de coordenadas geográficas. LPTS es la matriz de puntos en el espacio de la página, expresados como fracciones del BBox de la ventana gráfica para que sobrevivan al escalado. El elemento n de LPTS y el elemento n de GPTS nombran la misma ubicación física, una vez en coordenadas de página y otra en el globo. Tres o más de estos pares fijan la transformación afín, o en el caso general proyectiva, que mapea cualquier coordenada de página dentro de la ventana gráfica a una coordenada del mundo. Leerlos es cuestión de recorrer ambas matrices al mismo ritmo

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;

El diccionario de medición también informa sus unidades de visualización a través de GetMeasureDictPDU, que toma un UnitIndex de 1 para unidades lineales, 2 para unidades de área o 3 para unidades angulares y devuelve un código que identifica la unidad específica, por ejemplo, un metro o un pie internacional para la categoría lineal. La matriz Bounds, leída con GetMeasureDictBoundsItem, describe el cuadrilátero dentro de la ventana gráfica que realmente cubre la medición, que no siempre es el rectángulo completo

WKT frente a EPSG

La latitud y la longitud en GPTS no tienen sentido sin saber a qué sistema de coordenadas geográficas pertenecen, ya que una coordenada de 51.5, -0.1 cae en un lugar físico diferente bajo WGS 84 que bajo un datum nacional más antiguo. El diccionario de medición responde a esto a través de un diccionario de sistema de coordenadas, al que se llega con GetMeasureDictGCSDict para el sistema geográfico. El PDF describe ese sistema de una de dos maneras intercambiables, y un lector tiene que aceptar cualquiera de las dos

La primera es WKT, Well-Known Text, una cadena independiente que detalla el datum, el elipsoide, el meridiano de origen y las unidades por completo. Es detallada pero inequívoca y no necesita ninguna tabla de búsqueda externa. La segunda es un código EPSG, un único entero que indexa un sistema de coordenadas en el registro EPSG; 4326 es WGS 84, el marco que utiliza la mayoría de los datos de GPS de consumo. EPSG es compacto pero asume el lector puede resolver el código frente a una base de datos. Los archivos aparecen con uno, el otro o ambos, razón por la cual la API expone los tres métodos GetCSDictType, GetCSDictEPSG y GetCSDictWKT. GetCSDictType informa si el sistema es geográfico (un GEOGCS, valor de retorno 1) o proyectado (un PROJCS, valor de retorno 2), lo que le permite interpretar el resto correctamente antes de confiar en él

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;

Lectura del diccionario heredado LGIDict

Los archivos que son anteriores al modelo de ventana gráfica, o que fueron producidos por herramientas que aún emiten la codificación más antigua, llevan su registro en un LGIDict en la página en lugar de en un diccionario de medición. PDFlibPas informa cuántos de estos diccionarios tiene una página a través de GetPageLGIDictCount y devuelve el contenido sin procesar de cada uno con GetPageLGIDictContent, indexado desde uno. El texto devuelto es el diccionario tal como fue escrito, que contiene los campos de registro OGC 08-139r2, que su código luego analiza para recuperar el mismo tipo de mapeo de página a mundo que proporciona el diccionario de medición. En el lado de la escritura, AddLGIDictToPage adjunta un LGIDict a la página actual, para que un convertidor pueda procesar de ida y vuelta la forma heredada cuando un consumidor antiguo todavía la espera

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;

Cómo ensamblar la lectura

Un importador completo trata los dos esquemas como un par de pasadas sobre cada página. Seleccione la página, solicite a GetPageViewPortCount las ventanas gráficas ISO y, para cada ventana gráfica que posea un diccionario de medición, extraiga su BBox, sus matrices GPTS y LPTS, su unidad de datos de punto y la descripción de GCS a través del diccionario del sistema de coordenadas. Luego verifique GetPageLGIDictCount para ver si hay algún registro heredado que la pasada de ventana gráfica no cubrió. Un mapa que lleva ambos debería estar de acuerdo entre ellos; un mapa que solo lleva uno sigue resolviéndose, porque usted buscó en ambos lugares. Los controladores devueltos en el camino, ViewPortID, MeasureDictID, CSDictID, son enteros simples que siguen siendo válidos mientras el documento esté cargado, por lo que todo el recorrido es solo unos pocos bucles anidados sobre la lista de páginas sin asignaciones que gestionar

Una vez que puede recuperar el registro, la página se convierte en una fuente de datos en lugar de una imagen. Las técnicas complementarias para leer el resto de una página se cubren en el artículo sobre extracción de texto, imágenes y fuentes, y el procesamiento de una hoja georreferenciada en un dispositivo para mediciones en pantalla se describe en la guía práctica del contexto de dispositivo de impresión y vista previa. El lector geoespacial descrito aquí se distribuye como parte de la losLab PDF Library para Delphi y C++Builder, junto con las API de carga, extracción y renderizado cubiertas en otras secciones de este blog