Artykuł techniczny

Viewporty pomiarowe PDF dla rysunków skalowych i map GeoPDF

Plan parteru 1:100 wyeksportowany do PDF otwiera się w Acrobat, a narzędzie odległości zgłasza 5 m ściany południowej jako 1,97 in — jej długość na papierze. Brakującym kawałkiem jest viewport /VP, którego rectilinear (/RL) słownik miary przelicza punkty na metry — coś, co HotPDF zapisuje z Delphi, razem z geospatial (/GEO) punktami kontrolnymi dla map

Co viewport daje narzędziu pomiarowemu

Viewport to prostokąt na stronie, który przychodzi z własnym układem współrzędnych do pomiaru. Opcjonalny wpis /VP słownika strony (ISO 32000-1 tabela 30, PDF 1.6) trzyma tablicę słowników /Type /Viewport, a każdy potrzebuje tylko /BBox: /Name etykietuje go w interfejsie przeglądarki, a /Measure dostarcza skalę (ISO 32000-1 §12.9, tabela 260; ten sam słownik to tabela 265 w ISO 32000-2). Strona bez żadnego viewportu zostawia narzędzia pomiarowe Acrobat przy skali papieru 1:1, więc te 1,97 in to po prostu 50 mm tuszu zmierzone wierne. /BBox zapisuje się w default user space, lewy dolny róg, potem prawy górny, i ta kolejność rogów ustawia kierunek pomiarowych osi x i y, które standard deklaruje jako niezależne od /Rotate strony. Jeśli kiedyś spłaszczysz /Rotate w obrębie pudeł strony, każdy prostokąt /VP to kolejny kawałek geometrii, który musi pójść za zawartością

Viewporty mogą na siebie nachodzić, a §12.9 rozstrzyga nachodzenie kolejnością w tablicy: czytnik szukający viewportu pod punktem startuje od ostatniego wpisu /VP, iteruje w tył i zatrzymuje się na tym, którego /BBox zawiera punkt. Wtręt ze schodami 1:20 na arkuszu 1:100 musi więc pójść po viewportzie obejmującym cały arkusz — doklej go wcześniej, a duży prostokąt wygra każdy test trafienia, więc wtręt zmierzy się pięć razy za długo. Odległości idą za punktem startu. Linia zaczynająca się wewnątrz wtrętu i kończąca na głównym planie jest mierzona w 1:20 na całej swojej długości, z granicą albo bez

Tablica viewportów /VP w Delphi z HotPDF: AddRectilinearViewportEx dokleja wtręt ze schodami 1:20 po planie 1:100, więc odwrócony test trafienia wybiera wtręt, a linia 85 punktów odczytuje 0,60 m, nie 3,00 m
Doklejenie wtrętu ze schodami po viewportzie planu obejmującym arkusz pozwala odwróconemu przejściu po /VP znaleźć go jako pierwszego, utrzymując bieg schodów 85 punktów przy 0,60 m zamiast 3,00 m

Arytmetyka za skalą rectilinear

Słownik miary rectilinear (ISO 32000-1 tabela 262, tabela 267 w ISO 32000-2) ma cztery wymagane wpisy, i cała jego arytmetyka wisi na /C. /X, /D i /A to tablice formatów liczbowych: słowniki formatu liczbowego (tabela 263) uporządkowane od największej jednostki w dół, każdy z etykietą /U i przelicznikiem /C. Dalsze słowniki w tablicy przeliczają z jednostki przed nimi, podczas gdy słownik wiodący startuje z innego miejsca, zależnie od tego, który wpis jest właścicielem tablicy:

WpisWymaganyWiodący /C przelicza zArkusz metryczny 1:100
/RTakNic; proporcja jako tekst czytelny dla człowieka(1 cm = 1 m)
/XTakJednostki default user space/U (m) /C 0.0352778
/YGdy jednostki albo czynniki y się różniąJednostki default user spacePominięty
/DTakWiodąca jednostka /X/U (m) /C 1
/ATakWiodąca jednostka /X, do kwadratu/U (sq m) /C 1
/TNieStopniePominięty
/SNieWiodąca jednostka /Y podzielona przez wiodącą /XPominięty

Na metrycznym planie 1:100 skala mieszka w jednej liczbie. Jeden punkt to 25,4 / 72 = 0,352778 mm papieru, co oznacza 35,2778 mm budynku, więc /X dostaje /U (m) i /C 0.0352778, podczas gdy /D startuje z metrów, a /A z metrów kwadratowych, i oba dostają /C 1. Skopiowanie czynnika /X do /D i podniesienie go do kwadratu dla /A stosuje skalę podwójnie: ściana 5 m wraca jako 0,18 m, a pokój 20 m² kurczy się do 0,025 m². Zapisuj tę wiodącą jedynkę /C w /D i /A w każdym viewport, nawet tam, gdzie wygląda na redundantną. Tabela 262 czyni obie tablice wymaganymi i nie definiuje żadnego fallbacku, więc przeglądarka, która spotyka brakujący albo zgadywany czynnik, improwizuje, a dwie przeglądarki mogą improwizować na dwa różne sposoby; słownik zapisany w całości przypina każdy odczyt w samym pliku, a jednostki siedzą w kodzie, który recenzent może sprawdzić wobec tabliczki rysunku. Reszta modelu formatu liczbowego działa tak:

Łańcuch przeliczeń THPDFRectilinearMeasureInfo w HotPDF dla viewportu pomiarowego PDF 1:100 w Delphi: czynnik /X 0.0352778 z punktów na metry, czynniki /D i /A równe 1 oraz skurczone odczyty, gdy skala zostaje nałożona podwójnie
Tylko format liczbowy /X niesie skalę 1:100; przy /D i /A przypiętych na czynniku 1 ściana południowa odczytuje 5,00 m, a pokój 20,00 sq m
  • Łańcuchowe jednostki podlegają regule jednostki-poprzedniej, i tak przykład prosto ze standardu wypisuje "1 mi 2,378 ft 7 5/8 in" z milami przy /C 1, stopami przy /C 5280 i calami przy /C 12, z /F /F /D 8 zaokrągliającym do ósmych
  • /R to podpis dla ludzi, więc zły napis proporcji nad dobrym /C nadal daje dobre liczby
  • Profile z przerysowaną skalą pionową — przypadek "in X 1 cm = 1 m, in Y 1 cm = 30 m" z tabeli 262 — potrzebują /Y plus czynnika /CYX, który przelicza jednostki y na jednostki x; bez /CYX czytnik może odmówić liczenia odległości, pola i kąta

Pisanie planu 1:100 z wtrętem detali 1:20

THPDFPage.AddRectilinearViewportEx bierze prostokąt viewportu i rekord THPDFRectilinearMeasureInfo, którego pola mapują się na /R, /X, /Y, /D, /A, /T, /S, /O i /CYX, z każdą tablicą formatów liczbowych trzymaną jako rekordy THPDFNumberFormatEntry. Tabela z góry wchodzi bez zmian i nic nie zostaje do wywnioskowania czytelnikowi. Reguły wołania obowiązujące, zanim uruchomi się kod poniżej:

  • Viewport rectilinear potrzebuje PDF 1.6. HotPDF podnosi THotPDF.Version do pdf16 na żądanie; przy ustawionym StrictVersionLock metoda nic nie zapisuje i zwraca -1
  • Pusty /R albo tablica /X, /D lub /A bez wpisów podnosi wyjątek, bo tabela 262 wymaga wszystkich czterech
  • Każe wołanie dokleja do /VP, więc plan wchodzi przed wtręt, a wtręt ląduje tam, gdzie czytnik testuje go przed wszystkim innym
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 = setne
end;

// Rysunek metryczny w 1:Scale z każdym odczytem w metrach
function MetricScale(const Ratio: AnsiString;
  Scale: Double): THPDFRectilinearMeasureInfo;
const
  MetersPerPoint = 0.0254 / 72;                  // długość papierowa jednej jednostki
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);    // z jednostek X
  SetLength(Result.AreaFormats, 1);
  Result.AreaFormats[0] := NumberFormat('sq m', 1, 100);     // z jednostek X do kwadratu
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;
    // ... narysuj plan parteru i wtręt ze schodami ...

    // Prostokąty są w PDF default user space: punkty, początek lewy-dolny
    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;

Cztery argumenty prostokąta wchodzą do /BBox takimi, jakimi są, w PDF default user space: punkty, mierzone od lewego dolnego rogu. To przestrzeń, z której przelicza czynnik /X, więc prostokąt i skala dzielą jedną jednostkę. Wołania rysujące HotPDF używają innej, z początkiem u góry po lewej i współrzędnymi w 1/Resolution cala, i przepaść między jednostkami rysowania HotPDF a punktami PDF działa tutaj w pełnej krasie — wtręt rysowany we współrzędnych góra-lew potrzebuje wartości y odwróconych względem wysokości strony i przeliczonych na punkty, inaczej prostokąt pomiarowy ląduje na przeciwnej połowie arkusza. Strony w dużym formacie dokładają jeszcze jeden czynnik. Przy /UserUnit ustawionym przez THPDFPage.SetUserUnit jedna jednostka domyślna to UserUnit / 72 cala, więc czynnik /X skaluje się o tę samą wartość

Jakiej kolejności używają współrzędne GPTS?

Viewport mapy niesie słownik miary z /Subtype /GEO, który zaczynał jako rozszerzenie Adobe do PDF 1.7, a stał się częścią standardu w PDF 2.0 (ISO 32000-2 §12.10.2, tabela 269). /GCS jest wymagany i trzyma słownik /Type /GEOGCS (tabela 270) albo słownik /Type /PROJCS (tabela 271), identyfikowany kodem /EPSG albo napisem /WKT; dla układu geograficznego standard pozwala na jeden z tych dwóch i zakazuje obu naraz. Używaj EPSG, ilekroć układ współrzędnych ma kod. Liczby całkowitej nie zepsuje escaping napisów, a czytniki rozwiązują ją względem tego samego rejestru, z którego korzystają narzędzia GIS, podczas gdy WKT zostaje dla własnych projekcji, które kodu nie mają. /DCS opcjonalnie nazywa inny układ do wyświetlania pozycji. /PDU wypisuje preferowane jednostki wyświetlania z nazwy — liniowe, pola i kątowe, jak KM, SQKM i DEG — a HotPDF zapisuje ją tylko wtedy, gdy wszystkie trzy jednostki w THPDFGeoMeasureInfo są wypełnione

Pary /GPTS to szerokość, potem długość geograficzna, gdy /GCS jest geograficznym, i easting, potem northing, gdy jest projected. Kolejność się odwraca. Pary x/y z GIS wtrycione prosto w /GPTS wychodzą dobrze dla mapy UTM i źle dla WGS 84. Sam EPSG:4326 stawia szerokość przed długością, GeoJSON i większość API map internetowych robi odwrotnie, a zarejestrowana w kolejności web-map mapa ulic Zurychu pokazuje rezultat: 8,54° na wschód czyta się jako szerokość, 47,37° na północ jako długość, a kursor zgłasza pozycje gdzieś przy granicy Etiopii z Somalią. /LPTS trzyma pasujące punkty w kwadracie jednostkowym viewportu, gdzie (0, 0) to lewy dolny róg /BBox, a (1, 1) prawy górny, jedna para na parę /GPTS; wklejone tam współrzędne strony odstawiają punkty kontrolne setki kwadratów od mapy. /Bounds, neatline ograniczająca ważność georeferencji, używa tego samego kwadratu jednostkowego i domyślnie przyjmuje [0 0 0 1 1 1 1 0]. HotPDF sprawdza kształt, zanim cokolwiek zapisze: /GPTS potrzebuje parzystej liczby co najmniej czterech liczb, a niepusty /LPTS musi zgadzać się długością

Punkty kontrolne GeoPDF THPDFGeoMeasureInfo w Delphi z HotPDF: kolejność /GPTS szerokość-długość dla GEOGCS takiego jak EPSG 4326, easting-northing dla PROJCS oraz rogi /LPTS w kwadracie jednostkowym viewportu używanym przez AddGeospatialViewport
Układy współrzędnych geograficzne i projected porządkują pary /GPTS różnie, podczas gdy /LPTS zawsze liczy od lewego dolnego rogu /BBox viewportu w kwadracie jednostkowym

Przypinanie chmury punktów do viewportu mapy

Viewport GEO może też nieść słownik danych punktowych (ISO 32000-2 §12.10.5, tabela 272): /Type /PtData, /Subtype /Cloud, tablicę /Names działającą jako nagłówki kolumn i tablicę /XPTS z wierszami. LAT i LON w stopniach oraz ALT w metrach to nagłówki predefiniowane, a standard traktuje wiersze jako kolekcję bez kolejności. Strukturalnie /XPTS zagnieżdża się, podczas gdy /GPTS zostaje płaski: jedna wewnętrzna tablica na punkt, wymiarowana do /Names, czyli [[47.3769 8.5417 408] [47.3780 8.5400 410]]. THPDFPage.SetViewportPointDataCloud nadal przyjmuje jedną płaską listę — kształt, jaki produkuje eksport CSV — i kroi ją na wiersze według liczby kolumn; wartość parsująca się jako liczba z kropką dziesiętną staje się liczbą PDF, cokolwiek innego zostaje napisem, a liczba niebędąca wielokrotnością liczby kolumn podnosi wyjątek. Oba wołania potrzebują PDF 2.0, a indeks viewportu zwracany przez AddGeospatialViewport to miejsce, gdzie przypina się chmurę

procedure AddCityMap(Page: THPDFPage);
const
  // Rogi neatline, szerokość przed długością: 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);
  // Te same rogi w kwadracie jednostkowym viewportu
  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 zostaje puste
  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 trzyma z grubsza proporcję 12,8 km x 7,8 km mapy
  Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
  if Map < 0 then
    raise Exception.Create('GEO viewports need PDF 2.0');

  // Trzy kolumny, więc co trzy wartości stają się jednym wierszem /XPTS
  Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
    ['47.3769', '8.5417', '408',
     '47.3780', '8.5400', '410']);
end;

Odczytywanie viewportów i mierzenie na serwerze

THotPDF.GetLoadedPageViewports zwraca THPDFViewportInfoArray dla indeksu strony liczonego od zera, po jednym THPDFViewportInfo na wpis /VP w kolejności plikowej, z BBox, Name, MeasureSubtype, sparsowanym rekordem Rectilinear albo Geospatial i ewentualnymi wierszami PtData. Słownik miary pomijający /Subtype wraca jako RL — domyślną wartość, którą przydziela tabela 261 ISO 32000-1. Dwie funkcje poniżej odtwarzają to, co przeglądarka robi z kliknięciem: znajdź viewport odwróconym przejściem, a potem zamień odległość na stronie na prawdziwe jednostki przez /X (albo /Y z /CYX) i wiodący czynnik /D. Test granic używa Min i Max, bo rama pomiarowa obrócona względem strony może wypisywać swoje rogi w innej kolejności

uses
  SysUtils, Math, HPDFDoc;

// Idź po /VP w tył od ostatniego wpisu; wygrywa zawierający BBox osiągnięty najprędzej
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;

// Odległość między dwoma punktami strony, w wiodącej jednostce /D
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);            // decyduje punkt startowy
      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;

Oczekiwane wyjście dla arkusza zapisanego wcześniej: Stair detail 1:20 (1 cm = 20 cm): 0.60 m, bo 85 punktów po 0,0070556 m na punkt daje 0,5997 m. Zamień dwa wołania AddRectilinearViewportEx miejscami, a ta sama linia 85 punktów rozwiąże się na viewport planu i odczyta 3,00 m. Dla zeskanowanego zestawu rysunków, którego sam nie wyprodukowałeś, THotPDF.AddPageRectilinearViewportEx, AddPageGeospatialViewport i SetPageViewportPointDataCloud robią tę samą robotę na wczytanym dokumencie według indeksu strony przed SaveLoadedDocument. Adnotacje line, polyline i polygon mogą nieść własny /Measure (ISO 32000-1 tabele 175 i 178), a GetLoadedAnnotationMeasure parsuje go do tych samych rekordów RL i GEO, co ma znaczenie, gdy adnotacje pomiarowe w recenzowanym PDF przyszły z innego narzędzia

Przy /D i /A przypiętych na 1 czynnik /X równy 0.0352778 jest jedyną liczbą niosącą skalę 1:100, a ściana południowa 5 m powinna odczytywać się jako 5,00 m w każdym czytniku trzymającym się §12.9. Ten sam typ rekordu wraca z GetLoadedPageViewports, więc plik napisany przez HotPDF, komponent PDF dla Delphi da się wczytać ponownie i zestawić jego czynniki z wartościami, które wtrył twój kod