Technical Article

GeoPDF ב-Delphi: קואורדינטות גיאוגרפיות עם PDFlibPas

רוב המפתחים חושבים על עמוד PDF כעל גיליון נייר עם טקסט ותמונות עליו. קובץ PDF עם התייחסות גיאוגרפית (georeferenced PDF) הוא יותר מזה. הוא נושא מספיק מידע כדי לקחת נקודה בעמוד, הנמדדת ביחידות עמוד רגילות, ולדווח על קו הרוחב וקו האורך שהיא מייצגת בעולם האמיתי. עובדה יחידה זו היא מה שהופך את ה-PDF למוביל שמיש עבור מפה טופוגרפית, תרשים מדידה קדסטרי, תצוגת אזור הצפה, או כל ייצוא GIS שחייב להיות מודפס ועדיין להיות בעל משמעות. הגיאומטריה קיימת בקובץ; השאלה היחידה היא אם הטוען שלך קורא אותה.

הסיבה שזה מתפספס היא שקובץ GeoPDF נפתח ומודפס בדיוק כמו כל PDF אחר. שום דבר בעמוד המרונדר אינו מכריז שהמפה רשומה למערכת קואורדינטות. הרישום חי במילונים התלויים על אובייקט העמוד, שאינם מצוירים לעולם, ומציג שמתעלם מהם מראה לך את המפה בדיוק אותו הדבר. כדי לעשות משהו מרחבי (spatial) עם הקובץ – קריאת קואורדינטות מדידה, היטל מחדש (reprojection), שכבה מול שכבות אחרות – עליך לעבור על המילונים הללו בעצמך.

שני תקנים קיימים בשטח

קורא שרוצה לטפל בקבצים מהעולם האמיתי חייב להתמודד עם שתי סכמות רישום גיאוגרפיות, מכיוון ששתיהן נמצאות בשימוש וקובץ נתון עשוי להשתמש בכל אחת מהן. הישנה יותר היא קידוד ה-OGC המתואר ב-OGC 08-139r2, אשר מצרף LGIDict (מילון רישום גיאוגרפי) לעמוד. הוא קודם לכל אישור ISO והיה פורמט הדה-פקטו לפלט מוקדם של GeoPDF, ולכן גוף גדול של מפות ישנות נושא אותו ותו לא.

השיטה המודרנית היא זו ש-ISO תיקן ב-ISO 32000-1 §8.8.2. במקום מילון יחיד ברמת העמוד, היא ממדלת נתונים גיאוגרפיים כ-Viewport של עמוד עם מילון Measure מצורף, וממילון המדידה מציין מערכת קואורדינטות גיאוגרפית. זהו הקידוד ש-Acrobat ומייצאי GIS נוכחיים כותבים. יבואן חזק בודק את שניהם: קורא את ה-viewports עבור מודל ה-ISO, וחוזר ל-LGIDict (או בודק אותו בנוסף) עבור קבצים הנושאים רק את הרישום הישן.

Viewports והגבולות שלהם

במודל ה-ISO יחידת הרישום הגיאוגרפי היא ה-viewport, ועמוד עשוי לכלול כמה כאלה. גיליון גדול יכול להציב מפה ראשית במלבן אחד, מפה מוגדלת בקנה מידה אחר במלבן שני, ופאנל מקרא שאינו גיאורפרנסיבי כלל. כל viewport נושא BBox, המלבן בעמוד שה-viewport שולט עליו, כך שהקורא יודע איזה חלק מהגיליון מערכת קואורדינטות נתונה מייצגת. בדיקת פגיעה (Hit-testing) של נקודה שנלחצה מול תיבות אלה היא הדרך שבה מציג מחליט באיזה מילון מדידה להשתמש.

PDFlibPas חושף את ה-viewports של העמוד שנבחר ישירות. GetPageViewPortCount מחזיר כמה כאלה ישנם, GetPageViewPortID הופך אינדקס מבוסס 1 למזהה 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 פירושו שלא ניתן היה למצוא את ה-viewport באינדקס זה, לכן בדוק אותו לפני העברת המזהה הלאה.

בתוך מילון המדידה (measure dictionary)

הגיאומטריה שרושמת את העמוד לעולם חיה במילון המדידה המצורף ל-viewport. הפונקציה GetViewPortMeasureDict מחזירה MeasureDictID עבור ViewPortID נתון, או אפס כאשר ל-viewport אין מילון מדידה, שזה המקרה הרגיל עבור פאנל מקרא או כותרת. מילון המדידה מחזיק שלושה דברים ששווה לקרוא: מערכות הקואורדינטות שאליהן הוא מתייחס, המערכים הקושרים נקודות עמוד לנקודות גיאוגרפיות, והיחידה שבה מבוטאים נתוני הנקודה.

הרישום עצמו הוא שני מערכים מקבילים. GPTS הוא מערך הנקודות הגיאוגרפיות, זוגות של קווי רוחב ואורך הניתנים במערכת הקואורדינטות הגיאוגרפית. LPTS הוא מערך נקודות מרחב העמוד, המבוטאות כשברים של ה-BBox של ה-viewport כך שהן שורדות שינויי קנה מידה. פריט n של LPTS ופריט n of GPTS מציינים את אותו מיקום פיזי, פעם אחת בקואורדינטות עמוד ופעם אחת על הגלובוס. שלושה זוגות כאלה או יותר קובעים את ההתמרה האפינית (affine), או במקרה הכללי הפרויקטיבית, שממפה כל קואורדינטת עמוד בתוך ה-viewport לקואורדינטת עולם. קריאתם היא עניין של מעבר על שני המערכים במקביל.

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, מתאר את המרובע בתוך ה-viewport שהמדידה מכסה בפועל, שלאו דווקא מייצג את המלבן המלא.

WKT לעומת EPSG

קובי הרוחב והאורך ב-GPTS הם חסרי משמעות מבלי לדעת לאיזו מערכת קואורדינטות גיאוגרפית הם שייכים, שכן קואורדינטה של 51.5, -0.1 נוחתת בנקודה פיזית שונה תחת WGS 84 מאשר תחת נקודת ייחוס (datum) לאומית ישנה יותר. מילון המדידה עונה על כך באמצעות מילון מערכת קואורדינטות, שאליו מגיעים עם 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 הישן

קבצים הקודמים למודל ה-viewport, או שהופקו על ידי כלים שעדיין פולטים את הקידוד הישן יותר, נושאים את הרישום שלהם ב-LGIDict בעמוד במקום במילון מדידה. PDFlibPas מדווח כמה מילונים כאלה יש לעמוד דרך GetPageLGIDictCount ומחזיר את התוכן הגולמי של כל אחד מהם עם GetPageLGIDictContent, מאונדקס מ-1. הטקסט המוחזר הוא המילון כפי שנכתב, המחזיק את שדות הרישום של OGC 08-139r2, שהקוד שלך מנתח לאחר מכן כדי לשחזר את אותו סוג של מיפוי עמוד-לעולם שמילון המדידה מספק. בצד הכתיבה, AddLGIDictToPage מצרף LGIDict לעמוד הנוכחי, כך שממיר יכול לבצע סבב מלא (round-trip) של הצורה הישנה כאשר צרכן ישן עדיין מצפה לה.

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 את ה-viewports של ISO, ועבור כל viewport שבבעלותו מילון מדידה משוך את ה-BBox שלו, מערכי ה-GPTS וה-LPTS שלו, יחידת נתוני הנקודה שלו ותיאור ה-GCS דרך מילון מערכת הקואורדינטות. לאחר מכן בדוק את GetPageLGIDictCount עבור כל רישום ישן שמעבר ה-viewport לא כיסה. מפה הנושאת את שניהם צריכה להציג הסכמה ביניהם; מפה הנושאת רק אחד עדיין נפתרת, מכיוון שהסתכלת בשני המקומות. המזהים המוחזרים לאורך הדרך, ViewPortID,‏ MeasureDictID,‏ CSDictID, הם שלמים פשוטים שנשארים תקפים בזמן שהמסמך טעון, כך שכל המעבר הוא כמה לולאות מקוננות על פני רשימת העמודים ללא שום הקצאת זיכרון לניהול.

ברגע שאתה יכול לשחזר את הרישום, העמוד הופך למקור נתונים במקום לתמונה בלבד. טכניקות העזר לקריאת שאר העמוד מכוסות ב-מאמר על חילוץ טקסט, תמונות וגופנים, ורינדור גיליון גיאורפרנסיבי למכשיר עבור מדידה על המסך מתואר ב-מדריך להדפסה ותצוגה מקדימה של הקשר מכשיר (device context). הקורא הגיאוגרפי המתואר כאן נשלח כחלק מ-losLab PDF Library עבור Delphi ו-C++Builder לצד ממשקי ה-API לטעינה, חילוץ ורינדור המכוסים במקומות אחרים בבלוג זה.