Technical Article

GeoPDF у Delphi: геопросторові координати з PDFlibPas

Більшість розробників уявляють сторінку PDF як аркуш паперу з текстом і зображеннями. Геореференційований PDF - це набагато більше. Він містить достатньо інформації, щоб узяти точку на сторінці, виміряну в звичайних одиницях сторінки, і визначити широту й довготу, яким вона відповідає в реальному світі. Цей факт перетворює PDF на зручний носій для топографічної карти, плану кадастрової зйомки, схеми зон затоплення або будь-якого експорту ГІС, який потрібно надрукувати із збереженням просторових даних. Геометрія присутня у файлі; єдине питання полягає в тому, чи зчитує її ваш завантажувач.

Причина, чому це залишається непоміченим, полягає в тому, що GeoPDF відкривається і друкується точно так само, як і будь-який інший PDF. Ніщо на відтвореній сторінці не вказує на те, що карта прив'язана до системи координат. Прив'язка міститься в словниках, пов'язаних з об'єктом сторінки, які ніколи не відтворюються графічно, і переглядач, який ігнорує їх, все одно покаже вам карту. Щоб виконати будь-яку просторову операцію з файлом - зчитування координат геодезичної зйомки, перепроектування, накладання на інші шари - ви повинні самостійно обійти ці словники.

У практиці використовуються два стандарти

Зчитувач, який хоче працювати з реальними файлами, повинен підтримувати дві схеми геореєстрації, оскільки обидві знаходяться в обігу і конкретний файл може використовувати будь-яку з них. Старіша - це кодування OGC, описане в OGC 08-139r2, яке прикріплює до сторінки словник геопросторової реєстрації LGIDict. Воно з'явилося до схвалення ISO і було форматом de facto для ранніх версій GeoPDF, тому величезна кількість застарілих карт містить саме його.

Сучасна схема - це та, яку ISO стандартизувала в ISO 32000-1 §8.8.2. Замість одного словника на рівні сторінки вона моделює геопросторові дані як порт перегляду сторінки (Viewport) із прикріпленим словником вимірювань (Measure), і цей словник вимірювань визначає географічну систему координат. Це кодування використовують Acrobat та сучасні інструменти експорту ГІС. Надійний імпортер перевіряє обидва варіанти: зчитує порти перегляду для моделі ISO та переходить до перевірки LGIDict для файлів, які містять лише застарілу реєстрацію.

Порти перегляду та їхні межі

У моделі ISO одиницею геореєстрації є порт перегляду, і сторінка може мати кілька таких портів. Великий аркуш може містити основну карту в одному прямокутнику, врізку в іншому масштабі - в іншому, і панель легенди, яка взагалі не прив'язана до координат. Кожен порт перегляду містить BBox - прямокутник на сторінці, яким керує порт перегляду, завдяки чому зчитувач знає, до якої частини аркуша застосовується задана система координат. Перевірка потрапляння клікнутої точки в ці прямокутники - це спосіб, яким переглядач вибирає потрібний словник вимірювань.

PDFlibPas відкриває порти перегляду вибраної сторінки безпосередньо. Метод GetPageViewPortCount повертає їх кількість, GetPageViewPortID перетворює індекс (починаючи з одиниці) на дескриптор 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;

Значення ViewPortID, що дорівнює нулю, від GetPageViewPortID означає, що порт перегляду за цим індексом не знайдено, тому перевіряйте його перед передачею дескриптора далі.

Всередині словника вимірювань

Геометрія, яка прив'язує сторінку до фізичного світу, міститься в словнику вимірювань, прикріпленому до порту перегляду. Метод GetViewPortMeasureDict повертає MeasureDictID для заданого ViewPortID (або нуль, якщо порт перегляду не має словника вимірювань, що є звичайним для легенди чи панелі заголовка). Словник вимірювань містить три важливі речі: системи координат, на які він посилається, масиви, що пов'язують точки сторінки з географічними точками, та одиницю вимірювання даних точок.

Власне реєстрація - це два паралельних масиви. GPTS - це масив географічних точок, пар широти та довготи в географічній системі координат. LPTS - це масив точок у просторі сторінки, виражених у вигляді часток від BBox порту перегляду, що дозволяє їм переносити масштабування. Елемент n масиву LPTS та елемент n масиву GPTS вказують на одне й те саме фізичне місце: один раз у координатах сторінки, а другий - на земній кулі. Три або більше такі пари визначають афінне (або в загальному випадку проективне) перетворення, яке відображає будь-яку координату сторінки всередині порту перегляду на світову координату. Їх зчитування полягає в синхронному обході обох масивів.

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 для кутових одиниць) і повертає код, що ідентифікує конкретну одиницю, наприклад метр або міжнародний фут для лінійної категорії. Масив Bounds, що зчитується за допомогою GetMeasureDictBoundsItem, описує чотирикутник у межах порту перегляду, який фактично охоплює вимірювання (це не завжди весь прямокутник).

WKT проти EPSG

Широта й довгота в GPTS втрачають сенс без знання того, до якої географічної системи координат вони належать, оскільки координата 51.5, -0.1 вказує на різні фізичні місця в системі WGS 84 та в старішій національній системі координат. Словник вимірювань відповідає на це через словник системи координат, доступний за допомогою GetMeasureDictGCSDict для географічної системи. PDF описує цю систему одним із двох взаємозамінних способів, і зчитувач повинен підтримувати обидва.

Перший спосіб - це WKT (Well-Known Text), самодостатній рядок, який повністю описує геодезичну основу (datum), еліпсоїд, нульовий меридіан та одиниці вимірювання. Він є детальним, але однозначним і не потребує зовнішніх таблиць пошуку. Другий спосіб - це код EPSG, одне ціле число, яке індексує систему координат у реєстрі EPSG; наприклад, 4326 відповідає WGS 84, яку використовує більшість споживчих пристроїв GPS. Код EPSG є компактним, але вимагає від зчитувача вміння розпізнавати код за допомогою бази даних. Файли можуть містити один, інший або обидва варіанти, що пояснює, чому API надає доступ до всіх трьох методів: GetCSDictType, GetCSDictEPSG та GetCSDictWKT. Метод 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 і повертає необроблений вміст кожного за допомогою 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), є звичайними цілими числами, які залишаються дійсними, поки документ завантажений, тож весь процес обходу складається з кількох вкладених циклів за списком сторінок без потреби керування виділенням пам'яті.

Щойно ви отримуєте можливість відновити реєстрацію сторінки, вона перетворюється на джерело даних, а не просто на зображення. Супутні методи для читання решти вмісту сторінки описані в статті про вилучення тексту, зображень та шрифтів, а відтворення геореференційованого аркуша на пристрої для вимірювань на екрані описано в інструкції з контексту пристрою друку та попереднього перегляду. Геопросторовий зчитувач, описаний тут, постачається як частина бібліотеки losLab PDF Library для Delphi та C++Builder разом з API завантаження, вилучення та відтворення, описаними в інших статтях цього блогу.