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
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:
| Wpis | Wymagany | Wiodący /C przelicza z | Arkusz metryczny 1:100 |
|---|---|---|---|
| /R | Tak | Nic; proporcja jako tekst czytelny dla człowieka | (1 cm = 1 m) |
| /X | Tak | Jednostki default user space | /U (m) /C 0.0352778 |
| /Y | Gdy jednostki albo czynniki y się różnią | Jednostki default user space | Pominięty |
| /D | Tak | Wiodąca jednostka /X | /U (m) /C 1 |
| /A | Tak | Wiodąca jednostka /X, do kwadratu | /U (sq m) /C 1 |
| /T | Nie | Stopnie | Pominięty |
| /S | Nie | Wiodąca jednostka /Y podzielona przez wiodącą /X | Pominię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ń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.Versiondopdf16na żądanie; przy ustawionymStrictVersionLockmetoda 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ą
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