Artículo técnico

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. Transporta suficiente información para tomar un punto en la página, medido en unidades de página ordinarias, y reportar la latitud y longitud sobre las que se encuentra en el mundo real. Ese simple hecho es lo que convierte a un PDF en un portador útil para un mapa topográfico, un plano de levantamiento catastral, una exhibición de zona de inundación o cualquier exportación GIS (SIG) que deba 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 pasa desapercibido es que un GeoPDF se abre y se imprime exactamente como cualquier otro PDF. Nada en la página renderizada anuncia que el mapa está registrado en un sistema de coordenadas. El registro reside en diccionarios que cuelgan del objeto de la página, nunca se dibujan, y un visor que los ignora le muestra el mapa de todos modos. Para hacer cualquier cosa espacial con el archivo, examinar las lecturas de coordenadas, reproyección, superposición contra otras capas, usted tiene que recorrer esos diccionarios por sí mismo

Dos estándares viven en el entorno real

Un lector que quiera manejar archivos del mundo real tiene que lidiar con dos esquemas de georregistro, porque ambos están en circulación y un archivo determinado puede usar cualquiera de los dos. 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 ISO y fue el formato de facto para las primeras salidas de GeoPDF, por lo que una gran cantidad de mapas heredados lo transporta y nada más

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

Viewports y sus límites

En el modelo ISO, la unidad de georregistro es el viewport, y una página puede tener varios. 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 viewport lleva un BBox, el rectángulo en la página que rige el viewport, para que el lector sepa a qué parte de la hoja se aplica un sistema de coordenadas determinado. Realizar pruebas de colisión de un punto cliqueado contra esos cuadros es cómo un visor decide qué diccionario de medidas utilizar

PDFlibPas expone los viewports de la página seleccionada directamente. GetPageViewPortCount devuelve cuántos hay, GetPageViewPortID convierte un índice basado en uno en un handle 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 (Izquierda), 1 es Top (Arriba), 2 es Width (Ancho), 3 es Height (Alto), 4 es Right (Derecha), y 5 es Bottom (Abajo)

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 describen el área del mapa para este viewport
    end;
  finally
    Pdf.Free;
  end;
end;

Un ViewPortID de cero proveniente de GetPageViewPortID significa que el viewport en ese índice no pudo ser encontrado, por lo que debe verificarlo antes de pasar el handle

Dentro del diccionario de medidas

La geometría que registra la página en el mundo real reside en el diccionario de medidas adjunto a un viewport. GetViewPortMeasureDict devuelve un MeasureDictID para un ViewPortID dado, o cero cuando el viewport no tiene diccionario de medidas, lo cual es el caso normal para un panel de leyenda o título. El diccionario de medidas contiene tres cosas que vale la pena leer: los sistemas de coordenadas a los que hace referencia, los arreglos (arrays) que vinculan los puntos de la página a los puntos geográficos, y la unidad en la que se expresan los datos de los puntos

El registro en sí son dos arreglos paralelos. GPTS es el arreglo de puntos geográficos, pares de latitud y longitud dados en el sistema de coordenadas geográficas. LPTS es el arreglo de puntos del espacio de la página, expresados como fracciones del BBox del viewport para que sobrevivan a la escala. El elemento n de LPTS y el elemento n de GPTS nombran la misma ubicación física, una vez en coordenadas de la página y una vez en el globo. Tres o más de estos pares fijan la transformación afín, o en el caso general proyectiva, que asigna cualquier coordenada de página dentro del viewport a una coordenada del mundo. Leerlos es una cuestión de recorrer ambos arreglos al mismo paso

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 contiene pares lat/lon; LPTS contiene las fracciones de página correspondientes.
    // Ambos arreglos se leen con índices de elementos basados en uno.
    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) en la página corresponde a (lat, lon) en el terreno
      Inc(j, 2);
    end;
  end;
end;

El diccionario de medidas también reporta sus unidades de visualización a través de GetMeasureDictPDU, que toma un UnitIndex de 1 para unidades lineales, 2 para áreas, o 3 para unidades angulares y devuelve un código identificando la unidad específica, por ejemplo, un metro o un pie internacional para la categoría lineal. El arreglo Bounds, leído con GetMeasureDictBoundsItem, describe el cuadrilátero dentro del viewport que la medición realmente cubre, que no siempre es el rectángulo completo

WKT frente a EPSG

La latitud y longitud en GPTS carecen de sentido sin saber a qué sistema de coordenadas geográficas pertenecen, ya que una coordenada de 51.5, -0.1 aterriza en un punto físico diferente bajo WGS 84 que bajo un datum nacional más antiguo. El diccionario de medidas responde a esto a través de un diccionario de sistema de coordenadas, al que se accede con GetMeasureDictGCSDict para el sistema geográfico. PDF describe ese sistema de una de dos formas intercambiables, y un lector tiene que aceptar cualquiera de las dos

La primera es WKT (Well-Known Text), una cadena autónoma que detalla el datum, el elipsoide, el meridiano principal y las unidades por completo. Es detallada pero inequívoca y no necesita tabla de búsqueda externa. La segunda es un código EPSG, un solo entero que indexa un sistema de coordenadas en el registro EPSG; 4326 es WGS 84, el marco que usa la mayoría de los datos de GPS de consumo. EPSG es compacto pero asume que el lector puede resolver el código contra una base de datos. Los archivos aparecen con uno, con el otro, o con ambos, que es por lo que la API expone los tres: GetCSDictType, GetCSDictEPSG, y GetCSDictWKT. GetCSDictType reporta si el sistema es geográfico (un GEOGCS, valor de retorno 1) o proyectado (un PROJCS, valor de retorno 2), permitiéndole 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);   // ej. 4326 para WGS 84, 0 si está ausente
    wkt    := Pdf.GetCSDictWKT(gcsID);    // descripción de texto completo, '' si está ausente
    // Prefiera EPSG cuando esté presente; recurra a analizar WKT de lo contrario.
  end;
end;

Leyendo el LGIDict heredado

Los archivos anteriores al modelo de viewport, 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 medidas. PDFlibPas reporta 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 está escrito, conteniendo los campos de registro OGC 08-139r2, que luego su código analiza para recuperar el mismo tipo de mapeo de página a mundo que proporciona el diccionario de medidas. En el lado de la escritura, AddLGIDictToPage adjunta un LGIDict a la página actual, para que un convertidor pueda hacer la ida y vuelta del formato heredado cuando un consumidor antiguo aún lo espera

var
  lgiCount, k: Integer;
  dictText: WideString;
begin
  lgiCount := Pdf.GetPageLGIDictCount;
  for k := 1 to lgiCount do
  begin
    dictText := Pdf.GetPageLGIDictContent(k);
    // dictText lleva el registro OGC 08-139r2 a analizar
  end;
end;

Armando la lectura

Un importador completo trata los dos esquemas como un par de pasadas sobre cada página. Seleccione la página, consulte a GetPageViewPortCount por los viewports ISO, y para cada viewport que posea un diccionario de medidas, extraiga su BBox, sus arreglos GPTS y LPTS, su unidad de datos de punto y la descripción GCS a través del diccionario de sistema de coordenadas. Luego, verifique GetPageLGIDictCount en busca de cualquier registro heredado que la pasada de viewports no haya cubierto. Un mapa que lleva ambos debería coincidir entre ellos; un mapa que lleva solo uno aún se resuelve, porque buscó en ambos lugares. Los handles 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 son unos pocos bucles anidados sobre la lista de páginas sin tener que gestionar asignaciones

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 están cubiertas en el artículo sobre extracción de texto, imágenes y fuentes, y la renderización de una hoja georreferenciada a un dispositivo para medición en pantalla se describe en el recorrido paso a paso del contexto de dispositivo de impresión y vista previa. El lector geoespacial descrito aquí se incluye como parte de la Biblioteca PDF de losLab para Delphi y C++Builder, junto con las APIs de carga, extracción y renderización cubiertas en otros lugares de este blog