Teknik Makale

Ölçekli Çizimler ve GeoPDF Haritalar için PDF Viewport

PDF'e aktarılan 1:100 ölçekli bir kat planı Acrobat'ta açılır ve mesafe aracı, 5 m güney duvarını kağıttaki uzunluğu olan 1.97 in olarak raporlar. Eksik olan şey, point'leri metreye çeviren rectilinear (/RL) measure sözlüğüne sahip bir /VP viewport'tur — HotPDF bunu Delphi tarafında yazar; haritalar için geospatial (/GEO) kontrol noktalarıyla birlikte

Viewport'un ölçüm aracına verdiği şey

Viewport, sayfa üzerinde kendi ölçüm koordinat sistemini getiren bir dikdörtgendir. Page dictionary'nin isteğe bağlı /VP girdisi (ISO 32000-1 Table 30, PDF 1.6), /Type /Viewport sözlüklerinden oluşan bir dizi tutar ve her biri yalnızca bir /BBox'a ihtiyaç duyar: /Name onu viewer arayüzünde etiketler, /Measure ölçeği verir (ISO 32000-1 §12.9, Table 260; aynı sözlük ISO 32000-2'de Table 265'tir). Hiç viewport'u olmayan bir sayfa, Acrobat'ın ölçüm araçlarını 1:1 kağıt ölçeğinde bırakır; 1.97 in, sadakatle ölçülmüş 50 mm mürekkepten ibarettir. /BBox, default user space'te yazılır; önce sol alt köşe, sonra sağ üst. O köşe sırası, ölçüm x ve y eksenlerinin yönünü belirler ve standart bu eksenleri sayfanın /Rotate'inden bağımsız ilan eder. Sayfanın kutuları boyunca bir kez bile /Rotate'i düzleştirecek olursanız, her /VP dikdörtgeni içeriği izlemek zorunda olan bir geometri parçası daha olur

Viewport'lar üst üste binebilir ve §12.9 bu çakışmayı dizi sırasıyla çözer: bir noktanın altındaki viewport'u arayan okuyucu son /VP girdisinden başlar, geriye doğru yürür ve /BBox'ı o noktayı içeren girdide durur. Bu yüzden 1:100 bir palet üzerindeki 1:20 merdiven detayı, paleti kaplayan viewport'tan sonra gelmek zorundadır — onu daha önce eklerseniz büyük dikdörtgen her hit testini kazanır ve detay beş kat fazla uzun ölçülür. Mesafeler başlangıç noktalarını izler. Detayın içinde başlayıp ana plan üzerinde biten bir çizginin tamamı, sınırın içinden mi geçtiğine bakılmaksızın 1:20 üzerinden ölçülür

Delphi'de HotPDF /VP viewport dizisi: AddRectilinearViewportEx, 1:20 merdiven detayını 1:100 planın arkasına ekliyor; böylece ters hit test detayı seçiyor ve 85 point'lik bir çizgi 3.00 m değil 0.60 m okunuyor
Merdiven detayını, paleti kaplayan plan viewport'undan sonra eklemek ters /VP yürüyüşünün onu önce bulmasını sağlar; 85 point'lik merdiven kolu 3.00 m yerine 0.60 m kalır

Rectilinear ölçeğin arkasındaki aritmetik

Rectilinear measure sözlüğü (ISO 32000-1 Table 262, ISO 32000-2'de Table 267) dört zorunlu girdi taşır ve aritmetiğinin tamamı /C'ye asılıdır. /X, /D ve /A number format dizileridir: büyükten küçüğe dizilmiş number format sözlükleri (Table 263), her biri bir /U etiketi ve bir /C çevrim çarpanıyla. Dizideki sonraki sözlükler, kendilerinden önceki birimin üzerinden çevrim yapar; baştaki sözlük ise dizinin hangi girdiye ait olduğuna göre farklı bir yerden başlar:

GirdiZorunluBaştaki /C'nin çevirdiği kaynak1:100 metrik palet
/REvetHiçbir şey; insan tarafından okunabilen oran metni(1 cm = 1 m)
/XEvetDefault user space birimleri/U (m) /C 0.0352778
/Yy birimleri ya da çarpanlar farklılaştığındaDefault user space birimleriYok
/DEvetBaştaki /X birimi/U (m) /C 1
/AEvetBaştaki /X biriminin karesi/U (sq m) /C 1
/THayırDereceYok
/SHayırBaştaki /Y biriminin baştaki /X birimine bölümüYok

Metrik bir 1:100 planda ölçek tek bir sayıda yaşar. Bir point, 25.4 / 72 = 0.352778 mm kağıttır ve 35.2778 mm binayı temsil eder; bu yüzden /X, /U (m) ve /C 0.0352778 alır. /D metreden, /A metrekareden başlar ve ikisi de /C 1 alır. /X çarpanını /D'ye kopyalayıp /A için karesini almak, ölçeği iki kez uygular: 5 m duvar 0.18 m olarak geri döner ve 20 m² bir oda 0.025 m²'ye küçülür. Bu baştaki /C 1'i, gereksiz göründüğü yerlerde bile her viewport'ta /D'ye ve /A'ya yazın. Table 262 her iki diziyi de zorunlu kılar ve bir yedek tanımlamaz; eksik ya da uydurulmuş bir çarpana rastlayan viewer doğaçlama yapar ve iki viewer iki farklı biçimde doğaçlayabilir. Eksiksiz yazılmış bir sözlük, her okumayı dosyanın kendisine sabitler ve birimler, gözden geçirenin title block ile karşılaştırabileceği kodda durur. Number format modelinin geri kalanı şöyle çalışır:

Delphi'de 1:100 PDF ölçüm viewport'u için HotPDF THPDFRectilinearMeasureInfo çevrim zinciri: point'ten metreye /X çarpanı 0.0352778, /D ve /A çarpanları 1 ve ölçek iki kez uygulandığında küçülen okumalar
1:100 ölçeğini yalnızca /X number format'ı taşır; /D ve /A, 1 çarpanına sabitlenince güney duvarı 5.00 m, oda ise 20.00 sq m okunur
  • Zincirlenmiş birimler önceki-birim kuralını izler; standart kendi örneğinde de böyle yapar: /C 1 ile mil, /C 5280 ile feet ve /C 12 ile inch, /F /F /D 8 ile de sekizde birlere yuvarlanarak çıktıdaki "1 mi 2,378 ft 7 5/8 in" üretir
  • /R insanlar için bir başlıktır; doğru bir /C üzerine yazılmış yanlış bir oran string'i bile doğru sayıları verir
  • Dikey ölçeği abartılı profiller — Table 262'deki "in X 1 cm = 1 m, in Y 1 cm = 30 m" durumu — /Y'ye ek olarak y birimlerini x birimlerine çeviren bir /CYX çarpanı ister; /CYX yoksa okuyucu mesafe, alan ve açı hesaplamayı reddedebilir

1:20 detay içeren bir 1:100 plan yazmak

THPDFPage.AddRectilinearViewportEx, viewport dikdörtgenini ve alanları /R, /X, /Y, /D, /A, /T, /S, /O ve /CYX'e karşılık düşen bir THPDFRectilinearMeasureInfo kaydı alır; her number format dizisi THPDFNumberFormatEntry kayıtları halinde tutulur. Yukarıdaki tablo değişmeden içine girer ve okuyucuya çıkarım yapacak hiçbir şey bırakmaz. Aşağıdaki kod çalışmadan önce geçerli olan çağrı kuralları:

  • Bir rectilinear viewport PDF 1.6 ister. HotPDF, THotPDF.Version değerini gerektiğinde pdf16'ya yükseltir; StrictVersionLock etkinse metot hiçbir şey yazmaz ve -1 döndürür
  • Boş bir /R ya da girdisiz bir /X, /D, /A dizisi istisna fırlatır; Table 262 dördünü de zorunlu kılar
  • Her çağrı /VP'ye ekler; bu yüzden plan detaydan önce girer ve detay, okuyucunun ötekilerden önce sınadığı sıraya yerleşir
uses
  SysUtils, HPDFDoc;

function NumberFormat(const UnitLabel: AnsiString; Factor: Double;
  Precision: Integer): THPDFNumberFormatEntry;
begin
  Result := Default(THPDFNumberFormatEntry);
  Result.UnitLabel := UnitLabel;                 // /U
  Result.ConversionFactor := Factor;             // /C
  Result.FractionStyle := mnfDecimal;            // /F /D
  Result.Denominator := Precision;               // 100 = yüzde birler
end;

// 1:Scale'de metrik çizim; tüm okumalar metre cinsinden
function MetricScale(const Ratio: AnsiString;
  Scale: Double): THPDFRectilinearMeasureInfo;
const
  MetersPerPoint = 0.0254 / 72;                  // bir birimin kağıt üzerindeki uzunluğu
begin
  Result := Default(THPDFRectilinearMeasureInfo);
  Result.ScaleRatio := Ratio;                    // /R
  SetLength(Result.XFormats, 1);
  Result.XFormats[0] := NumberFormat('m', MetersPerPoint * Scale, 1000);
  SetLength(Result.DistanceFormats, 1);
  Result.DistanceFormats[0] := NumberFormat('m', 1, 100);    // X birimlerinden
  SetLength(Result.AreaFormats, 1);
  Result.AreaFormats[0] := NumberFormat('sq m', 1, 100);     // X birimlerinin karesinden
end;

procedure WritePlanSheet(const FileName: string);
var
  Pdf: THotPDF;
  Plan, Detail: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := FileName;
    Pdf.BeginDoc;
    // ... kat planını ve merdiven detayını çizin ...

    // Dikdörtgenler PDF default user space'tedir: point cinsinden, orijin sol altta
    Plan := Pdf.CurrentPage.AddRectilinearViewportEx('Ground floor 1:100',
      36, 36, 576, 756, MetricScale('1 cm = 1 m', 100));
    Detail := Pdf.CurrentPage.AddRectilinearViewportEx('Stair detail 1:20',
      396, 72, 576, 252, MetricScale('1 cm = 20 cm', 20));
    if (Plan < 0) or (Detail < 0) then
      raise Exception.Create('StrictVersionLock keeps the file below PDF 1.6');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dört dikdörtgen argümanı olduğu gibi /BBox'a gider; PDF default user space'indedirler, yani point cinsinden ve sol alt köşeden ölçülür. /X çarpanının çevirdiği uzay tam da budur; dikdörtgen ile ölçek aynı birimi paylaşır. HotPDF çizim çağrıları ise başka bir uzay kullanır: orijin sol üsttedir ve koordinatlar 1/Resolution inch cinsindendir. HotPDF çizim birimleri ile PDF point'leri arasındaki fark burada tam gücüyle geçerlidir — sol üst koordinatlarıyla çizilen bir detayın y değerleri sayfa yüksekliğine karşı çevrilip point'e dönüştürülmelidir, yoksa ölçüm dikdörtgeni paletin öbür yarısına düşer. Büyük format sayfalar bir çarpan daha ekler. THPDFPage.SetUserUnit ile /UserUnit atandığında bir default birim, UserUnit / 72 inch'tir; dolayısıyla /X çarpanı aynı değerle ölçeklenir

GPTS koordinatları hangi sırayı kullanır?

Bir harita viewport'u, /Subtype /GEO taşıyan bir measure sözlüğü getirir; bu, PDF 1.7'ye bir Adobe uzantısı olarak başladı ve PDF 2.0'da standart oldu (ISO 32000-2 §12.10.2, Table 269). /GCS zorunludur ve bir /Type /GEOGCS sözlüğü (Table 270) ya da bir /Type /PROJCS sözlüğü (Table 271) tutar; ikisi de bir /EPSG kodu ya da bir /WKT string'iyle tanımlanır. Coğrafi bir sistem için standart ikiden birine izin verir ve ikisinin bir arada bulunmasını yasaklar. Koordinat sisteminin bir kodu varsa her zaman EPSG kullanın. Bir integer, string kaçış dizileriyle bozulamaz ve okuyucular onu, GIS araçlarının kullandığı aynı kayıt defterine göre çözer; WKT ise kodsuz kalan özel projeksyonlar için kalır. /DCS isteğe bağlı olarak konum gösterimi için başka bir sistem adlandırır. /PDU, tercih edilen gösterim birimlerini adlarıyla listeler — KM, SQKM ve DEG gibi lineer, alan ve açısal birimler — ve HotPDF bunu, THPDFGeoMeasureInfo içindeki üç birim de doldurulduğunda yazar

/GPTS çiftleri, /GCS coğrafiyken önce enlem sonra boylam; projekteyken doğu-kuzey (easting-northing) sırasındadır. Sıra değişir. Doğrudan /GPTS'e beslenen GIS x/y çiftleri bir UTM haritası için doğru, WGS 84 için yanlış çıkar. EPSG:4326 kendisi enlemi boylamın önüne koyar; GeoJSON ve çoğu web harita API'si tersini yapar ve web harita sırasıyla kaydedilmiş bir Zürih sokak haritası sonucu gösterir: 8.54° doğu bir enlem olarak, 47.37° kuzey bir boylam olarak okunur ve imleç, Etiyopya-Somali sınırı yakınlarındaki konumları raporlar. /LPTS, eşleşen noktaları viewport'un birim karesinde tutar: (0, 0) /BBox'ın sol alt köşesi, (1, 1) sağ üst köşesidir ve her /GPTS çiftine bir çift düşer; oraya yapıştırılan sayfa koordinatları kontrol noktalarını haritadan yüzlerce kare uzağa gönderir. Georeference'ın geçerli olduğu yeri sınırlayan neatline olan /Bounds da aynı birim kareyi kullanır ve [0 0 0 1 1 1 1 0] değerine varsayılanlaşır. HotPDF, bir şey yazmadan önce şekli denetler: /GPTS, en az dört sayılı çift sayıda değer ister ve boş olmayan bir /LPTS uzunluğunu eşlemek zorundadır

Delphi'de HotPDF THPDFGeoMeasureInfo GeoPDF kontrol noktaları: EPSG 4326 gibi bir GEOGCS için /GPTS enlem-boylam sırası, bir PROJCS için easting-northing ve AddGeospatialViewport'un kullandığı viewport birim karesinde /LPTS köşeleri
Coğrafi ve projekte koordinat sistemleri /GPTS çiftlerini farklı sıralar; /LPTS ise her zaman, birim kare içinde viewport /BBox'ının sol alt köşesinden sayar

Bir harita viewport'una nokta bulutu eklemek

Bir GEO viewport'u bir nokta veri sözlüğü de taşıyabilir (ISO 32000-2 §12.10.5, Table 272): /Type /PtData, /Subtype /Cloud, sütun başlıkları gibi işleyen bir /Names dizisi ve satırlardan oluşan bir /XPTS dizisi. Derece cinsinden LAT ve LON ile metre cinsinden ALT önceden tanımlı başlıklardır ve standart satırları sırasız bir koleksiyon sayar. Yapısal olarak /XPTS iç içe geçerken /GPTS düz kalır: nokta başına bir iç dizi, /Names'e göre boyutlanır; yani [[47.3769 8.5417 408] [47.3780 8.5400 410]]. THPDFPage.SetViewportPointDataCloud yine de tek bir düz liste kabul eder — bir CSV çıktısının ürettiği şekil — ve sütun sayısına göre satırlara böler; nokta ondalık ayracıyla sayı olarak parse edilen bir değer PDF sayısına dönüşür, ötekiler string olarak kalır ve sütun sayısının katı olmayan bir sayı istisna fırlatır. Her iki çağrı da PDF 2.0 ister ve bulut, AddGeospatialViewport'un döndürdüğü viewport indeksine eklenir

procedure AddCityMap(Page: THPDFPage);
const
  // Neatline köşeleri, enlem boylamdan önce: NW, NE, SE, SW
  CornerLatLon: array[0..7] of Double =
    (47.40, 8.45,  47.40, 8.62,  47.33, 8.62,  47.33, 8.45);
  // Aynı köşeler viewport birim karesinde
  CornerUnit: array[0..7] of Double =
    (0, 1,  1, 1,  1, 0,  0, 0);
var
  Geo: THPDFGeoMeasureInfo;
  Map, I: Integer;
begin
  Geo := Default(THPDFGeoMeasureInfo);
  Geo.GCS.CSType := cstGEOGCS;
  Geo.GCS.EPSG := 4326;                  // WGS 84; WKT boş kalır
  Geo.LinearUnit := 'KM';
  Geo.AreaUnit := 'SQKM';
  Geo.AngularUnit := 'DEG';
  SetLength(Geo.GPTS, Length(CornerLatLon));
  SetLength(Geo.LPTS, Length(CornerUnit));
  for I := 0 to High(CornerLatLon) do
  begin
    Geo.GPTS[I] := CornerLatLon[I];
    Geo.LPTS[I] := CornerUnit[I];
  end;

  // 540 x 324 pt, haritanın 12.8 km x 7.8 km oranını kabaca korur
  Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
  if Map < 0 then
    raise Exception.Create('GEO viewports need PDF 2.0');

  // Üç sütun; her üç değer bir /XPTS satırı olur
  Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
    ['47.3769', '8.5417', '408',
     '47.3780', '8.5400', '410']);
end;

Viewport'ları geri okumak ve sunucuda ölçmek

THotPDF.GetLoadedPageViewports, sıfır tabanlı bir sayfa indeksi için bir THPDFViewportInfoArray döndürür: dosya sırasına göre her /VP girdisi için bir THPDFViewportInfo; BBox, Name, MeasureSubtype, parse edilmiş Rectilinear ya da Geospatial kaydı ve varsa PtData satırlarıyla. /Subtype'ı atlayan bir measure sözlüğü, ISO 32000-1 Table 261'in atadığı varsayılan olan RL olarak geri gelir. Aşağıdaki iki fonksiyon, bir viewer'ın bir tıklamayla yaptığını yeniden üretir: ters yürüyüşle viewport'u bulmak, sonra sayfa üzerindeki bir mesafeyi /X (ya da /CYX'li /Y) ve baştaki /D çarpanı üzerinden gerçek birimlere çevirmek. Sınır testi Min ile Max kullanır; çünkü sayfaya göre döndürülmüş bir ölçüm çerçevesi köşelerini başka bir sırayla listeleyebilir

uses
  SysUtils, Math, HPDFDoc;

// /VP'yi son girdiden geriye doğru gez; noktayı içeren BBox'a en erken ulaşan kazanır
function ViewportAt(const VPs: THPDFViewportInfoArray; X, Y: Double): Integer;
var
  I: Integer;
begin
  for I := High(VPs) downto 0 do
    if (X >= Min(VPs[I].BBoxLeft, VPs[I].BBoxRight)) and
       (X <= Max(VPs[I].BBoxLeft, VPs[I].BBoxRight)) and
       (Y >= Min(VPs[I].BBoxBottom, VPs[I].BBoxTop)) and
       (Y <= Max(VPs[I].BBoxBottom, VPs[I].BBoxTop)) then
      Exit(I);
  Result := -1;
end;

// İki sayfa noktası arasındaki mesafe, baştaki /D birimi cinsinden
function MeasuredDistance(const VP: THPDFViewportInfo;
  X1, Y1, X2, Y2: Double): Double;
var
  RL: THPDFRectilinearMeasureInfo;
  DX, DY: Double;
begin
  RL := VP.Rectilinear;
  if (Length(RL.XFormats) = 0) or (Length(RL.DistanceFormats) = 0) then
    raise Exception.Create('Viewport has no usable /X or /D array');
  DX := (X2 - X1) * RL.XFormats[0].ConversionFactor;
  if Length(RL.YFormats) > 0 then
  begin
    if not RL.HasCYX then
      raise Exception.Create('/Y without /CYX: distance is undefined');
    DY := (Y2 - Y1) * RL.YFormats[0].ConversionFactor * RL.CYX;
  end
  else
    DY := (Y2 - Y1) * RL.XFormats[0].ConversionFactor;
  Result := Sqrt(Sqr(DX) + Sqr(DY)) * RL.DistanceFormats[0].ConversionFactor;
end;

procedure MeasureStairRun;
var
  Pdf: THotPDF;
  VPs: THPDFViewportInfoArray;
  Hit: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('floor-plan.pdf') > 0 then
    begin
      VPs := Pdf.GetLoadedPageViewports(0);
      Hit := ViewportAt(VPs, 450, 150);            // kararı başlangıç noktası verir
      if (Hit >= 0) and (VPs[Hit].MeasureSubtype = 'RL') then
        Writeln(Format('%s (%s): %.2f m', [string(VPs[Hit].Name),
          string(VPs[Hit].Rectilinear.ScaleRatio),
          MeasuredDistance(VPs[Hit], 450, 150, 450, 235)]));
    end;
  finally
    Pdf.Free;
  end;
end;

Daha önce yazılan palet için beklenen çıktı: Stair detail 1:20 (1 cm = 20 cm): 0.60 m; çünkü point başına 0.0070556 m ile 85 point, 0.5997 m eder. İki AddRectilinearViewportEx çağrısının yerini değiştirin ve aynı 85 point'lik çizgi plan viewport'una düşer, 3.00 m okur. Üretmediğiniz taranmış bir çizim seti için THotPDF.AddPageRectilinearViewportEx, AddPageGeospatialViewport ve SetPageViewportPointDataCloud, aynı işi yüklenmiş bir belge üzerinde SaveLoadedDocument öncesinde sayfa indeksiyle yapar. Çizgi, polyline ve polygon annotation'ları kendi /Measure'lerini taşıyabilir (ISO 32000-1 Tables 175 ve 178) ve GetLoadedAnnotationMeasure bunu aynı RL ve GEO kayıtlarına parse eder; bu, gözden geçirilmiş bir PDF'teki ölçüm annotation'ları başka bir araçtan geldiğinde işe yarar

/D ve /A, 1'e sabitlendiğinde 1:100 ölçeğini taşıyan tek sayı, 0.0352778'lik /X çarpanıdır ve §12.9'u izleyen herhangi bir okuyucuda 5 m güney duvarı 5.00 m okumalıdır. Aynı kayıt türü GetLoadedPageViewports'tan da geri gelir; böylece HotPDF Delphi PDF component ile yazılmış bir dosya yeniden yüklenebilir ve çarpanları, kodunuzun içine yazdığı değerlerle karşılaştırılabilir