Een plattegrond van de begane grond op 1:100, geëxporteerd naar PDF, opent in Acrobat en de afstandstool meldt de 5 m zuidmuur als 1.97 in, zijn lengte op papier. Het ontbrekende onderdeel is een /VP-viewport waarvan de rectilinear-measure-dictionary (/RL) punten naar meters omrekent — iets dat HotPDF vanuit Delphi wegschrijft, samen met geospatial-control points (/GEO) voor kaarten
Wat een viewport de meettool geeft
Een viewport is een rechthoek op de pagina die zijn eigen meetsysteem van coördinaten meebrengt. De optionele /VP-entry van de page-dictionary (ISO 32000-1 Tabel 30, PDF 1.6) bevat een array van /Type /Viewport-dictionaries, en elk ervan heeft alleen een /BBox nodig: /Name benoemt hem in de interface van de viewer en /Measure levert de schaal (ISO 32000-1 §12.9, Tabel 260; dezelfde dictionary is Tabel 265 in ISO 32000-2). Een pagina zonder enige viewport houdt de meettools van Acrobat op een papieren schaal 1:1, dus die 1.97 in is gewoon 50 mm inkt, trouw gemeten. /BBox staat in default user space, hoek linksonder eerst en dan rechtsboven, en die hoekvolgorde bepaalt de richting van de meetassen x en y, die de standaard onafhankelijk verklaart van de /Rotate van de pagina. Flatten ooit /Rotate over de boxes van een pagina, dan is elke /VP-rechthoek nog een stuk geometrie dat met de inhoud mee moet
Viewports mogen overlappen, en §12.9 lost de overlap op via de array-volgorde: een reader die de viewport onder een punt zoekt begint bij de laatste /VP-entry, loopt achterwaarts en stopt bij de eerste waarvan de /BBox de punt bevat. Een trapdetail-inzet op 1:20 moet op een vel van 1:100 dus ná het vel-brede viewport komen — eerder toegevoegd en de grote rechthoek wint elke hit-test, waardoor het detail vijf keer te lang meet. Afstanden volgen hun startpunt. Een lijn die binnen de inzet begint en buiten op de hoofdplattegrond eindigt wordt over zijn hele lengte op 1:20 gemeten, rand of geen rand
De rekenkunde achter een rectilinear-schaal
De rectilinear-measure-dictionary (ISO 32000-1 Tabel 262, Tabel 267 in ISO 32000-2) heeft vier verplichte entries, en al zijn rekenwerk hangt aan /C. /X, /D en /A zijn number format-arrays: number format-dictionaries (Tabel 263) geordend van de grootste eenheid naar beneden, elk met een /U-label en een /C-omrekenfactor. Latere dictionaries in een array rekenen om vanuit de eenheid vóór hen, terwijl de leidende dictionary vanaf een andere plek begint, afhankelijk van welke entry de array bezit:
| Entry | Verplicht | Leidende /C rekent om vanuit | Metrisch vel op 1:100 |
|---|---|---|---|
| /R | Ja | Niets; ratio-tekst voor mensen | (1 cm = 1 m) |
| /X | Ja | Default user space-eenheden | /U (m) /C 0.0352778 |
| /Y | Als y-eenheden of -factoren verschillen | Default user space-eenheden | Weggelaten |
| /D | Ja | De leidende /X-eenheid | /U (m) /C 1 |
| /A | Ja | De leidende /X-eenheid, gekwadrateerd | /U (sq m) /C 1 |
| /T | Nee | Graden | Weggelaten |
| /S | Nee | Leidende /Y-eenheid gedeeld door leidende /X-eenheid | Weggelaten |
Op een metrische plattegrond 1:100 woont de schaal in één getal. Eén point is 25.4 / 72 = 0.352778 mm papier, dat staat voor 35.2778 mm gebouw, dus /X krijgt /U (m) en /C 0.0352778, terwijl /D vanaf meters begint en /A vanaf vierkante meters, en beiden /C 1 krijgen. De /X-factor in /D kopiëren en voor /A kwadrateren past de schaal twee keer toe: de 5 m muur komt terug als 0.18 m en een kamer van 20 m² krimpt tot 0.025 m². Schrijf die leidende /C van 1 in /D en /A bij elke viewport, ook waar hij overbodig lijkt. Tabel 262 maakt beide arrays verplicht en definieert geen fallback, dus een viewer die een ontbrekende of gegokte factor tegenkomt improviseert, en twee viewers kunnen op twee manieren improviseren; een dictionary die volledig is uitgeschreven zet elke readout vast in het bestand zelf, en de eenheden zitten in code die een reviewer tegen het titelblok kan afvinken. De rest van het number format-model werkt zo:
- Geketende eenheden volgen de vorige-eenheid-regel, en zo print het eigen voorbeeld van de standaard "1 mi 2,378 ft 7 5/8 in" vanuit miles op /C 1, feet op /C 5280 en inches op /C 12, met /F /F /D 8 dat op achten afrondt
- /R is een bijschrift voor mensen, dus een verkeerde ratio-string boven een juiste /C levert nog steeds juiste getallen
- Profielen met een overdreven verticale schaal, het geval "in X 1 cm = 1 m, in Y 1 cm = 30 m" uit Tabel 262, hebben /Y plus een /CYX-factor nodig die y-eenheden naar x-eenheden omrekent; zonder /CYX kan een reader weigeren afstand, oppervlak en hoek te berekenen
Een plattegrond 1:100 schrijven met een detail-inzet op 1:20
THPDFPage.AddRectilinearViewportEx neemt de viewport-rechthoek en een record THPDFRectilinearMeasureInfo waarvan de velden op /R, /X, /Y, /D, /A, /T, /S, /O en /CYX afbeelden, met elke number format-array vastgehouden als records THPDFNumberFormatEntry. De tabel hierboven gaat ongewijzigd naar binnen, en er blijft de reader niets meer om af te leiden. De aanroepregels die gelden voordat de code hieronder draait:
- Een rectilinear-viewport heeft PDF 1.6 nodig. HotPDF tilt
THotPDF.Versionop verzoek naarpdf16; metStrictVersionLockgezet schrijft de methode niets en geeft -1 terug - Een lege /R, of een array /X, /D of /A zonder entries, gooit een exception, want Tabel 262 eist alle vier
- Elke aanroep voegt aan /VP toe, dus de plattegrond gaat erin vóór de inzet en de inzet eindigt waar een reader hem vóór al het andere test
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 = honderdsten
end;
// Metrische tekening op 1:Scale met elke readout in meters
function MetricScale(const Ratio: AnsiString;
Scale: Double): THPDFRectilinearMeasureInfo;
const
MetersPerPoint = 0.0254 / 72; // papierlengte van één eenheid
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); // vanuit X-eenheden
SetLength(Result.AreaFormats, 1);
Result.AreaFormats[0] := NumberFormat('sq m', 1, 100); // vanuit X-eenheden gekwadrateerd
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;
// ... teken de plattegrond van de begane grond en de trapdetail-inzet ...
// Rechthoeken zijn PDF default user space: points, oorsprong linksonder
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;
De vier rechthoekargumenten gaan ongewijzigd in /BBox, in PDF default user space: points, gemeten vanaf de hoek linksonder. Dat is de ruimte waar de /X-factor vandaan rekent, dus rechthoek en schaal delen één eenheid. HotPDF-tekenaanroepen gebruiken een andere, met de oorsprong linksboven en coördinaten in 1/Resolution inch, en het gat tussen HotPDF-tekenenheden en PDF-points geldt hier in volle omvang — een inzet getekend op linksboven-coördinaten heeft y-waarden nodig die tegen de paginahoogte gespiegeld en naar points omgerekend zijn, anders belandt de meetrechthoek op de tegenovergestelde helft van het vel. Grootformaatpagina's voegen nog een factor toe. Met /UserUnit gezet via THPDFPage.SetUserUnit is één default-eenheid UserUnit / 72 inch, dus de /X-factor schaalt mee met diezelfde waarde
Welke volgorde gebruiken GPTS-coördinaten?
Een kaart-viewport draagt een measure-dictionary met /Subtype /GEO, dat begon als Adobe-uitbreiding op PDF 1.7 en in PDF 2.0 deel van de standaard werd (ISO 32000-2 §12.10.2, Tabel 269). /GCS is verplicht en bevat een dictionary /Type /GEOGCS (Tabel 270) of een dictionary /Type /PROJCS (Tabel 271), geïdentificeerd via een /EPSG-code of een /WKT-string; voor een geografisch systeem staat de standaard één van de twee toe en verbiedt ze samen. Gebruik EPSG zodra het coördinatensysteem een code heeft. Een integer kan niet door string-escaping kapot, en readers zetten hem af tegen hetzelfde register dat GIS-tools gebruiken, terwijl WKT blijft voor de custom-projecties zonder code. /DCS benoemt optioneel een ander systeem voor het tonen van posities. /PDU lijst preferred display units op naam — lineair, oppervlak en hoek, zoals KM, SQKM en DEG — en HotPDF schrijft hem alleen als alle drie de eenheden in THPDFGeoMeasureInfo zijn ingevuld
/GPTS-paren zijn latitude dan longitude wanneer /GCS geografisch is, en easting dan northing wanneer hij geprojecteerd is. De volgorde wisselt. GIS x/y-paren rechtstreeks in /GPTS gestopt komen voor een UTM-kaart goed uit en voor WGS 84 verkeerd. EPSG:4326 zelf zet latitude vóór longitude, GeoJSON en de meeste web map-API's doen het omgekeerd, en een stratenkaart van Zürich geregistreerd in web-map-volgorde laat het resultaat zien: 8.54° oost wordt als latitude gelezen, 47.37° noord als longitude, en de cursor meldt posities bij de grens Ethiopië–Somalië. /LPTS bevat de matchende punten in het unit square van de viewport, waar (0, 0) de linksonder van /BBox is en (1, 1) de rechtsboven, één paar per /GPTS-paar; paginacoördinaten daar geplakt zetten de control points honderden vakken naast de kaart. /Bounds, de neatline die begrenst waar de georeferentie geldig is, gebruikt hetzelfde unit square en valt default terug op [0 0 0 1 1 1 1 0]. HotPDF controleert de vorm voordat hij iets schrijft: /GPTS heeft een even aantal van ten minste vier getallen nodig, en een niet-lege /LPTS moet in lengte matchen
Een point cloud aan een kaart-viewport hangen
Een GEO-viewport mag ook een point data-dictionary dragen (ISO 32000-2 §12.10.5, Tabel 272): /Type /PtData, /Subtype /Cloud, een /Names-array die als kolomkoppen dient en een /XPTS-array van rijen. LAT en LON in graden en ALT in meters zijn vooraf gedefinieerde koppen, en de standaard behandelt de rijen als een ongeordende verzameling. Structureel nest /XPTS terwijl /GPTS plat blijft: één binnenarray per punt, op maat van /Names, dus [[47.3769 8.5417 408] [47.3780 8.5400 410]]. THPDFPage.SetViewportPointDataCloud accepteert ook één platte lijst, de vorm die een CSV-export oplevert, en snijdt hem op kolomaantal in rijen; een waarde die parseert als getal met een punt als decimaalteken wordt een PDF-number, al het andere blijft een string, en een aantal dat geen veelvoud van het kolomaantal is gooit een exception. Beide aanroepen hebben PDF 2.0 nodig, en de viewport-index die AddGeospatialViewport teruggeeft is waar de cloud vastzit
procedure AddCityMap(Page: THPDFPage);
const
// Neatline-hoeken, latitude vóór longitude: NW, NO, ZO, ZW
CornerLatLon: array[0..7] of Double =
(47.40, 8.45, 47.40, 8.62, 47.33, 8.62, 47.33, 8.45);
// Dezelfde hoeken in het viewport-unit square
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 blijft leeg
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 houdt ongeveer de 12.8 km x 7.8 km verhouding van de kaart aan
Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
if Map < 0 then
raise Exception.Create('GEO viewports need PDF 2.0');
// Drie kolommen, dus elke drie waarden worden één /XPTS-rij
Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
['47.3769', '8.5417', '408',
'47.3780', '8.5400', '410']);
end;
Viewports teruglezen en op de server meten
THotPDF.GetLoadedPageViewports geeft een THPDFViewportInfoArray terug voor een zero-based pagina-index, één THPDFViewportInfo per /VP-entry in bestandsvolgorde, met de BBox, Name, MeasureSubtype, het geparseerde record Rectilinear of Geospatial en eventuele PtData-rijen. Een measure-dictionary die /Subtype weglaat komt terug als RL, de default die ISO 32000-1 Tabel 261 toekent. De twee functies hieronder reproduceren wat een viewer bij een klik doet: vind de viewport met de achterwaartse walk, en zet dan een pagina-afstand in echte eenheden om via /X (of /Y met /CYX) en de leidende /D-factor. De bounds-test gebruikt Min en Max want een meetkader gedraaid tegen de pagina kan zijn hoeken in een andere volgorde noemen
uses
SysUtils, Math, HPDFDoc;
// Loop /VP achterwaarts vanaf de laatste entry; de bereikte BBox die het eerst omvat wint
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;
// Afstand tussen twee pagina-punten, in de leidende /D-eenheid
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); // het startpunt bepaalt
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;
Verwachte uitvoer voor het vel dat eerder werd weggeschreven: Stair detail 1:20 (1 cm = 20 cm): 0.60 m, want 85 points à 0.0070556 m per point is 0.5997 m. Wissel de twee aanroepen AddRectilinearViewportEx om en dezelfde lijn van 85 point valt op het plattegrond-viewport en leest 3.00 m. Voor een gescande tekeningenset die u niet zelf produceerde doen THotPDF.AddPageRectilinearViewportEx, AddPageGeospatialViewport en SetPageViewportPointDataCloud hetzelfde werk op een geladen document per pagina-index vóór SaveLoadedDocument. Line-, polyline- en polygon-annotations kunnen hun eigen /Measure dragen (ISO 32000-1 Tabellen 175 en 178), en GetLoadedAnnotationMeasure parseert die naar dezelfde RL- en GEO-records, wat telt wanneer de measurement-annotations in een gereviewde PDF uit een andere tool kwamen
Met /D en /A vast op 1 is de /X-factor van 0.0352778 het enige getal dat de schaal 1:100 draagt, en de 5 m zuidmuur hoort in elke reader die §12.9 volgt als 5.00 m te lezen. Hetzelfde recordtype komt terug uit GetLoadedPageViewports, dus een bestand geschreven door de HotPDF Delphi PDF component kan opnieuw worden geladen en zijn factoren zijn te vergelijken met de waarden die uw code erin zette