Technical Article

GeoPDF ใน Delphi: พิกัดภูมิสารสนเทศด้วย PDFlibPas

นักพัฒนาส่วนใหญ่มักคิดว่าหน้ากระดาษ PDF เป็นเพียงแผ่นกระดาษที่มีตัวอักษรและรูปภาพวางอยู่ แต่ไฟล์ PDF อ้างอิงทางภูมิศาสตร์ (georeferenced PDF) มีคุณค่าเหนือกว่านั้น มันบรรจุข้อมูลที่ละเอียดเพียงพอที่จะจับตำแหน่งจุดใด ๆ บนหน้ากระดาษซึ่งคำนวณในหน่วยทั่วไปของหน้ากระดาษ และรายงานพิกัดละติจูดและลองจิจูดจริงบนพื้นโลกของจุดดังกล่าวออกมา ข้อเท็จจริงข้อนี้คือสิ่งที่เปลี่ยนไฟล์ PDF ให้เป็นตัวขนส่งข้อมูลแผนที่ภูมิประเทศ (topographic map) แผนผังการสำรวจโฉนดที่ดิน แผนผังแสดงเขตอุทกภัย หรือผลลัพธ์ของ GIS ใด ๆ ที่ต้องการความหมายจริงจังเมื่อสั่งพิมพ์ ข้อมูลเชิงเรขาคณิตเหล่านี้บันทึกอยู่ภายในไฟล์เรียบร้อยแล้ว คำถามเพียงอย่างเดียวคือตัวโหลดข้อมูลของคุณได้เปิดอ่านมันขึ้นมาหรือไม่

สาเหตุที่ประเด็นนี้มักถูกมองข้ามก็เพราะไฟล์ GeoPDF สามารถเปิดอ่านและพิมพ์ออกเครื่องพิมพ์ได้ปกติเหมือนไฟล์ PDF ทั่วไป ไม่มีสิ่งใดที่ถูกเรนเดอร์ในหน้ากระดาษคอยบอกว่าแผนที่นี้ถูกตรึงกับระบบพิกัดทางภูมิศาสตร์ การเชื่อมโยงข้อมูลพิกัดจะบันทึกอยู่ในพจนานุกรม (dictionaries) ที่เชื่อมโยงเข้ากับอ็อบเจกต์หน้ากระดาษโดยที่ไม่มีการแสดงผลการวาดใด ๆ และโปรแกรมแสดงผลที่มองข้ามข้อมูลเหล่านี้ก็จะยังคงแสดงแผนที่ให้คุณเห็นได้ปกติ หากคุณต้องการดำเนินการใด ๆ ในเชิงมิติพิกัดกับตัวไฟล์ เช่น การอ่านพิกัดสำรวจ การเปลี่ยนฉายแผนที่ใหม่ (reprojection) หรือการวางซ้อนแผนที่ทับบนชั้นข้อมูล (layer) อื่น ๆ คุณจำเป็นต้องเขียนโค้ดไล่อ่านข้อมูลพจนานุกรมเหล่านั้นด้วยตนเอง

มาตรฐานสองแบบที่มีใช้งานจริง

โปรแกรมอ่านข้อมูลที่ต้องการจัดการกับไฟล์ที่ใช้งานจริงบนพื้นโลกจำเป็นต้องรองรับรูปแบบการเชื่อมโยงพิกัดภูมิศาสตร์ (georegistration) สองรูปแบบ เนื่องจากมีข้อมูลทั้งสองแบบไหลเวียนอยู่และไฟล์ที่ได้รับมาอาจเลือกใช้งานแบบใดก็ได้ รูปแบบเก่าคือการเข้ารหัสแบบ OGC ที่อธิบายใน OGC 08-139r2 ซึ่งจะผูกข้อมูลพจนานุกรม LGIDict (พจนานุกรมพิกัดภูมิสารสนเทศ) เข้ากับหน้ากระดาษ รูปแบบนี้เกิดขึ้นมาก่อนมาตรฐาน ISO และเคยเป็นโครงสร้างมาตรฐานหลักสำหรับผลลัพธ์ GeoPDF รุ่นแรก แผนที่เวอร์ชันเก่า ๆ จำนวนมากจึงยังคงบันทึกในรูปแบบนี้เป็นหลัก

รูปแบบสมัยใหม่คือมาตรฐานที่ได้รับการรับรองจาก ISO ใน ISO 32000-1 §8.8.2 แทนที่จะเก็บเป็นพจนานุกรมตัวเดียวในระดับหน้ากระดาษ มันจะจำลองข้อมูลพิกัดทางภูมิศาสตร์ให้อยู่ในรูปพอร์ตการมองเห็นหน้ากระดาษ (Viewport) ควบคู่กับพจนานุกรมการวัดขนาด (Measure) และพจนานุกรมการวัดขนาดจะทำหน้าที่ตั้งชื่อระบบพิกัดทางภูมิศาสตร์ นี่คือการเข้ารหัสข้อมูลที่โปรแกรม Acrobat และตัวส่งออก GIS ในปัจจุบันเลือกใช้ โปรแกรมนำเข้าข้อมูลที่ดีควรตรวจสอบทั้งสองจุด: ให้อ่านจากพอร์ตการมองเห็นในโมเดล ISO ก่อน และหากไม่พบค่อยดึงข้อมูลย้อนกลับ (หรือตรวจวิเคราะห์ควบคู่) ไปยัง LGIDict สำหรับไฟล์ประเภทที่เก็บข้อมูลเฉพาะรูปแบบเก่าดั้งเดิม

พอร์ตการมองเห็นและขอบเขต

ในโมเดลมาตรฐาน ISO หน่วยย่อยของการเชื่อมโยงพิกัดคือพอร์ตการมองเห็น (viewport) และในหนึ่งหน้ากระดาษสามารถประกาศพอร์ตการมองเห็นได้หลายตัว แผ่นกระดาษแผ่นใหญ่สามารถวางแผนที่หลักไว้ในสี่เหลี่ยมผืนผ้ารูปหนึ่ง วางแผนที่ย่อยย่อส่วนที่มีระดับมาตราส่วนต่างออกไปในสี่เหลี่ยมผืนผ้าอีกรูปหนึ่ง และมีแผงคำอธิบายสัญลักษณ์แผนที่ (legend) ที่ไม่มีระบบอ้างอิงพิกัดเลย พอร์ตการมองเห็นแต่ละตัวจะมาพร้อมข้อมูล BBox ซึ่งก็คือสี่เหลี่ยมผืนผ้าบนหน้ากระดาษที่พอร์ตการมองเห็นนั้นควบคุมดูแล เพื่อให้โปรแกรมอ่านทราบว่าระบบพิกัดนั้นมีผลใช้งานอยู่บนขอบเขตส่วนใดของหน้ากระดาษ การรันคำสั่งตรวจสอบจุดคลิก (hit-testing) เทียบกับขอบเขตสี่เหลี่ยมเหล่านี้คือวิธีที่โปรแกรมแสดงผลใช้ตัดสินใจว่าควรนำพจนานุกรมการวัดขนาดตัวใดมาใช้งาน

PDFlibPas exposes the viewports of the selected page directly. GetPageViewPortCount returns how many there are, GetPageViewPortID turns a one-based index into a ViewPortID handle, and GetViewPortBBox reads the bounding rectangle one dimension at a time. The Dimension argument selects which edge or extent you want: 0 is Left, 1 is Top, 2 is Width, 3 is Height, 4 is Right, and 5 is 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 จะอ้างอิงตำแหน่งจริงทางกายภาพตำแหน่งเดียวกัน โดยตัวแรกระบุในพิกัดหน้ากระดาษและตัวหลังระบุเป็นพิกัดจริงบนผิวโลก การมีจุดพิกัดคู่กันตั้งแต่สามคู่ขึ้นไปจะเพียงพอสำหรับการคำนวณการแปลงพิกัด (affine transform หรือในกรณีทั่วไปคือ projective transform) เพื่อจับคู่จากพิกัดใด ๆ บนหน้ากระดาษภายใต้พอร์ตการมองเห็นเข้ากับพิกัดจริงบนพื้นโลก การอ่านข้อมูลจึงเป็นการวิเคราะห์ไล่ดูอาร์เรย์ทั้งสองชุดคู่ขนานกันไป:

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 เทียบกับระบบพิกัดอ้างอิงเก่าของแต่ละประเทศ พจนานุกรมการวัดขนาดจะระบุข้อมูลเรื่องนี้ไว้ในพจนานุกรมระบบพิกัด (coordinate system dictionary) ซึ่งสามารถเข้าถึงได้ผ่านฟังก์ชัน GetMeasureDictGCSDict สถาปัตยกรรมเอกสาร PDF จะระบุระบบอ้างอิงทางภูมิศาสตร์ผ่านวิธีการที่แลกเปลี่ยนกันได้สองรูปแบบ และโปรแกรมอ่านจำเป็นต้องสามารถรองรับได้ทั้งสองระบบ

รูปแบบแรกคือสตริงข้อความประเภท WKT (Well-Known Text) ซึ่งเป็นสตริงชุดข้อความสมบูรณ์ที่ระบุค่าพื้นฐานรูปทรงโลก (datum) ทรงรีสัณฐานโลก (ellipsoid) เส้นเมอริเดียนแรก (prime meridian) และหน่วยวัดอย่างครบถ้วน แม้จะมีความยาวข้อความค่อนข้างยาวแต่มีความชัดเจนสูงและไม่จำเป็นต้องพึ่งพาตารางค้นหาภายนอก รูปแบบที่สองคือรหัส 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 จะทำหน้าที่ผูกโครงสร้าง AddLGIDictToPage เข้ากับหน้ากระดาษปัจจุบัน เพื่อรองรับกระบวนการแปลงไฟล์ย้อนกลับ (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 เพื่อดึงพอร์ตการมองเห็นในโมเดล ISO และสำหรับพอร์ตแต่ละตัวที่มีพจนานุกรมการวัดขนาด ให้ดึงข้อมูล BBox อาร์เรย์ GPTS และ LPTS หน่วยข้อมูลจุด และรายละเอียดระบบพิกัด GCS ผ่านทางพจนานุกรมระบบพิกัด หลังจากนั้นให้รันการตรวจสอบฟังก์ชัน GetPageLGIDictCount เพื่ออ่านข้อมูลอ้างอิงพิกัดแบบเก่าดั้งเดิมที่โมเดล ISO อาจไม่ได้ครอบคลุม แผนที่แผนผังที่เก็บข้อมูลพิกัดทั้งสองระบบควรมีพิกัดที่สอดคล้องตรงกัน แต่หากแผนที่เก็บข้อมูลเพียงระบบเดียวก็ยังสามารถประมวลผลวิเคราะห์ได้ เนื่องจากคุณได้ตรวจสอบจากทั้งสองจุดเรียบร้อยแล้ว ตัวจัดการต่าง ๆ ที่ส่งคืนกลับมาตลอดกระบวนการ ไม่ว่าจะเป็น ViewPortID, MeasureDictID และ CSDictID ล้วนเป็นเลขจำนวนเต็มธรรมดาที่ยังคงใช้งานได้ดีตลอดระยะเวลาการโหลดเอกสาร ดังนั้นขั้นตอนการไล่อ่านข้อมูลทั้งหมดจึงเป็นเพียงแค่ลูปซ้อนลูปง่าย ๆ ในรายการหน้ากระดาษโดยไม่มีขั้นตอนจองพื้นที่เพิ่มเติมให้ต้องจัดการคอยดูแล

เมื่อคุณสามารถดึงพิกัดภูมิศาสตร์กลับมาได้ หน้าเอกสารกระดาษก็จะเปลี่ยนหน้าที่จากรูปภาพเปล่า ๆ กลายเป็นแหล่งข้อมูลดิจิทัลที่ใช้งานได้จริง เทคนิคเสริมสำหรับการอ่านข้อมูลส่วนอื่น ๆ ของหน้าเอกสารระบุไว้ใน บทความการดึงข้อความ รูปภาพ และฟอนต์ของเรา และส่วนการแสดงผลเอกสารอ้างอิงภูมิศาสตร์ออกสู่อุปกรณ์เพื่อคำนวณวัดระยะบนหน้าจอมีอธิบายไว้ใน คู่มือการพรีวิวและการพิมพ์ในบริบทอุปกรณ์ ตัวอ่านข้อมูลเชิงพื้นที่ทางภูมิศาสตร์ที่อธิบายไว้ที่นี่มีพร้อมใช้งานในฐานะส่วนหนึ่งของ losLab PDF Library สำหรับ Delphi และ C++Builder ร่วมกับ API การโหลด การแยกข้อมูล และการแสดงผลที่ระบุไว้ในบล็อกนี้