Una planimetria del piano terra in scala 1:100 esportata in PDF si apre in Acrobat, e lo strumento distanza riporta il muro sud da 5 m come 1.97 in, la sua lunghezza sulla carta. Il pezzo mancante è un viewport /VP il cui dizionario di misura rectilinear (/RL) converte i punti in metri — una cosa che HotPDF scrive da Delphi, insieme ai punti di controllo geospatial (/GEO) per le mappe
Cosa offre un viewport allo strumento di misura
Un viewport è un rettangolo sulla pagina che porta con sé il proprio sistema di coordinate di misura. La voce opzionale /VP del dizionario di pagina (ISO 32000-1 Tabella 30, PDF 1.6) contiene un array di dizionari /Type /Viewport, e ciascuno richiede solo un /BBox: /Name lo etichetta nell'interfaccia del viewer e /Measure fornisce la scala (ISO 32000-1 §12.9, Tabella 260; lo stesso dizionario è la Tabella 265 in ISO 32000-2). Una pagina senza viewport lascia gli strumenti di misura di Acrobat su una scala carta 1:1, quindi l'1.97 in è semplicemente 50 mm d'inchiostro misurati fedelmente. /BBox è scritto in default user space, angolo in basso a sinistra e poi in alto a destra, e quell'ordine degli angoli fissa la direzione degli assi x e y di misura, che lo standard dichiara indipendenti dal /Rotate della pagina. Se vi capita di appiattire il /Rotate sui box di una pagina, ogni rettangolo /VP è un altro pezzo di geometria che deve seguire il contenuto
I viewport possono sovrapporsi, e il §12.9 risolve la sovrapposizione per ordine di array: un reader che cerca il viewport sotto un punto parte dall'ultima voce /VP, itera all'indietro e si ferma a quella il cui /BBox contiene il punto. Un dettaglio di scala 1:20 su un foglio 1:100 va quindi dopo il viewport esteso a tutto il foglio — se lo accodate prima, il grande rettangolo vince ogni hit test, e il dettaglio misura cinque volte troppo. Le distanze seguono il loro punto di partenza: una linea che inizia dentro l'inserto e finisce fuori, sul disegno generale, viene misurata a 1:20 su tutta la sua lunghezza, con o senza bordo
L'aritmetica dietro una scala rectilinear
Il dizionario di misura rectilinear (ISO 32000-1 Tabella 262, Tabella 267 in ISO 32000-2) ha quattro voci obbligatorie, e tutta la sua aritmetica pende da /C. /X, /D e /A sono array di number format: dizionari di number format (Tabella 263) ordinati dall'unità più grande in giù, ciascuno con un'etichetta /U e un fattore di conversione /C. I dizionari successivi in un array convertono dall'unità che li precede, mentre il primo dizionario parte da un punto diverso a seconda della voce a cui appartiene l'array:
| Voce | Obbligatoria | La /C iniziale converte da | Foglio metrico 1:100 |
|---|---|---|---|
| /R | Sì | Nulla; testo del rapporto leggibile dalle persone | (1 cm = 1 m) |
| /X | Sì | Unità del default user space | /U (m) /C 0.0352778 |
| /Y | Quando unità o fattori di y differiscono | Unità del default user space | Omeso |
| /D | Sì | L'unità /X iniziale | /U (m) /C 1 |
| /A | Sì | L'unità /X iniziale, al quadrato | /U (sq m) /C 1 |
| /T | No | Gradi | Omeso |
| /S | No | Unità /Y iniziale divisa per l'unità /X iniziale | Omeso |
Su una planimetria metrica 1:100 la scala sta in un numero. Un punto è 25.4 / 72 = 0.352778 mm di carta, che rappresenta 35.2778 mm di edificio, quindi /X prende /U (m) e /C 0.0352778, mentre /D parte dai metri e /A dai metri quadri e entrambi prendono /C 1. Copiare il fattore /X dentro /D ed elevarlo al quadrato per /A applica la scala due volte: il muro da 5 m torna come 0.18 m, e una stanza di 20 m² si riduce a 0.025 m². Scrivete quella /C iniziale di 1 dentro /D e /A su ogni viewport, anche dove sembra ridondante. La Tabella 262 rende entrambi gli array obbligatori e non definisce alcun ripiego, quindi un viewer che incontra un fattore mancante o indovinato sta improvvisando, e due viewer possono improvvisare in due modi diversi; un dizionario scritto per intero fissa ogni lettura nel file stesso, e le unità stanno in un codice che chi revisiona può confrontare con il cartiglio. Il resto del modello di number format funziona così:
- Le unità in catena seguono la regola dell'unità precedente, ed è così che l'esempio dello standard stesso stampa "1 mi 2,378 ft 7 5/8 in" da miglia a /C 1, piedi a /C 5280 e pollici a /C 12, con /F /F /D 8 che arrotonda agli ottavi
- Il /R è una didascalia per le persone, quindi una stringa di rapporto sbagliata sopra un /C giusto produce comunque numeri giusti
- I profili con scala verticale esagerata, il caso "in X 1 cm = 1 m, in Y 1 cm = 30 m" della Tabella 262, richiedono /Y più un fattore /CYX che converta le unità di y in unità di x; senza /CYX un reader può rifiutarsi di calcolare distanza, area e angolo
Scrivere una planimetria 1:100 con un inserto di dettaglio 1:20
THPDFPage.AddRectilinearViewportEx prende il rettangolo del viewport e un record THPDFRectilinearMeasureInfo i cui campi mappano su /R, /X, /Y, /D, /A, /T, /S, /O e /CYX, con ogni array di number format tenuto come record THPDFNumberFormatEntry. La tabella qui sopra va dentro invariata, e nulla resta da inferire per il reader. Le regole di chiamata che valgono prima che il codice qui sotto giri:
- Un viewport rectilinear richiede PDF 1.6. HotPDF alza
THotPDF.Versionapdf16su domanda; conStrictVersionLockimpostato, il metodo non scrive nulla e restituisce -1 - Un /R vuoto, o un array /X, /D o /A senza voci, solleva un'eccezione, visto che la Tabella 262 esige tutti e quattro
- Ogni chiamata accoda a /VP, quindi la planimetria va dentro prima dell'inserto e l'inserto finisce nel punto in cui un reader lo testa davanti a tutto il resto
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 = centesimi
end;
// Disegno metrico a 1:Scale con ogni lettura in metri
function MetricScale(const Ratio: AnsiString;
Scale: Double): THPDFRectilinearMeasureInfo;
const
MetersPerPoint = 0.0254 / 72; // lunghezza sulla carta di una unità
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); // da unità X
SetLength(Result.AreaFormats, 1);
Result.AreaFormats[0] := NumberFormat('sq m', 1, 100); // da unità X al quadrato
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;
// ... disegnate la planimetria del piano terra e l'inserto della scala ...
// I rettangoli sono in PDF default user space: punti, origine in basso a sinistra
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;
I quattro argomenti del rettangolo vanno in /BBox così come sono, in PDF default user space: punti, misurati dall'angolo in basso a sinistra. È lo spazio da cui il fattore /X converte, quindi rettangolo e scala condividono una sola unità. Le chiamate di disegno di HotPDF usano un'altra unità, con l'origine in alto a sinistra e coordinate in pollici da 1/Resolution, e il divario fra unità di disegno HotPDF e punti PDF qui vale per intero — un inserto disegnato con coordinate alto-sinistra ha bisogno dei suoi valori di y ribaltati contro l'altezza della pagina e convertiti in punti, altrimenti il rettangolo di misura atterra sulla metà opposta del foglio. Le pagine in grande formato aggiungono un fattore ulteriore: con /UserUnit impostato tramite THPDFPage.SetUserUnit, una unità default vale UserUnit / 72 di pollice, quindi il fattore /X scala dello stesso valore
Quale ordine usano le coordinate GPTS?
Un viewport di mappa porta un dizionario di misura con /Subtype /GEO, nato come estensione Adobe al PDF 1.7 e diventato parte dello standard nel PDF 2.0 (ISO 32000-2 §12.10.2, Tabella 269). /GCS è obbligatorio e contiene un dizionario /Type /GEOGCS (Tabella 270) o un dizionario /Type /PROJCS (Tabella 271), identificato da un codice /EPSG o da una stringa /WKT; per un sistema geografico lo standard ne permette uno dei due e vieta entrambi insieme. Usate EPSG ogni volta che il sistema di coordinate ha un codice: un intero non può essere corrotto dall'escaping di stringhe, e i reader lo risolvono nello stesso registro che usano gli strumenti GIS, mentre il WKT resta per le proiezioni personalizzate prive di codice. /DCS nomina opzionalmente un altro sistema per mostrare le posizioni. /PDU elenca per nome le unità di visualizzazione preferite — lineari, di area e angolari, come KM, SQKM e DEG — e HotPDF la scrive solo quando tutte e tre le unità in THPDFGeoMeasureInfo sono compilate
Le coppie /GPTS sono latitudine poi longitudine quando /GCS è geografico, ed easting poi northing quando è proiettato. L'ordine si ribalta. Coppie x/y GIS immerse direttamente in /GPTS vengono fuori giuste per una mappa UTM e sbagliate per WGS 84. EPSG:4326 stesso mette la latitudine avanti alla longitudine, GeoJSON e la maggior parte delle API di web map fanno l'opposto, e una mappa stradale di Zurigo registrata in ordine web map mostra il risultato: 8.54° est viene letto come latitudine, 47.37° nord come longitudine, e il cursore riporta posizioni vicino al confine Etiopia–Somalia. /LPTS contiene i punti corrispondenti nel quadrato unitario del viewport, dove (0, 0) è l'angolo in basso a sinistra di /BBox e (1, 1) quello in alto a destra, una coppia per ogni coppia /GPTS; coordinate di pagina incollate lì mettono i punti di controllo a centinaia di quadrati dalla mappa. /Bounds, la neatline che limita dove la georeferenziazione è valida, usa lo stesso quadrato unitario e vale [0 0 0 1 1 1 1 0] per default. HotPDF controlla la forma prima di scrivere qualsiasi cosa: /GPTS richiede un numero pari di valori, almeno quattro, e una /LPTS non vuota deve eguagliarne la lunghezza
Attaccare una nuvola di punti a un viewport di mappa
Un viewport GEO può anche portare un dizionario di dati puntuali (ISO 32000-2 §12.10.5, Tabella 272): /Type /PtData, /Subtype /Cloud, un array /Names che funziona da intestazioni di colonna e un array /XPTS di righe. LAT e LON in gradi e ALT in metri sono intestazioni predefinite, e lo standard tratta le righe come una collezione non ordinata. Strutturalmente /XPTS si annida mentre /GPTS resta piatto: un array interno per punto, dimensionato su /Names, quindi [[47.3769 8.5417 408] [47.3780 8.5400 410]]. THPDFPage.SetViewportPointDataCloud accetta comunque una sola lista piatta, la forma che produce un export CSV, e la affetta in righe secondo il numero di colonne; un valore che si fa il parse come numero con separatore decimale punto diventa un numero PDF, tutto il resto resta una stringa, e un conteggio che non è multiplo del numero di colonne solleva un'eccezione. Entrambe le chiamate richiedono PDF 2.0, e l'indice di viewport che AddGeospatialViewport restituisce è dove la nuvola si aggancia
procedure AddCityMap(Page: THPDFPage);
const
// Angoli della neatline, latitudine prima della longitudine: 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);
// I stessi angoli nel quadrato unitario del viewport
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 resta vuoto
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 conserva all'incirca l'aspetto 12.8 km x 7.8 km della mappa
Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
if Map < 0 then
raise Exception.Create('GEO viewports need PDF 2.0');
// Tre colonne, così ogni tre valori diventano una riga /XPTS
Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
['47.3769', '8.5417', '408',
'47.3780', '8.5400', '410']);
end;
Rileggere i viewport e misurare sul server
THotPDF.GetLoadedPageViewports restituisce un THPDFViewportInfoArray per un indice di pagina a base zero, un THPDFViewportInfo per voce /VP in ordine di file, con il BBox, il Name, il MeasureSubtype, il record Rectilinear o Geospatial già fatto il parse e le eventuali righe PtData. Un dizionario di misura che omette /Subtype torna come RL, il default che la Tabella 261 di ISO 32000-1 assegna. Le due funzioni qui sotto riproducono quello che un viewer fa con un click: trovare il viewport con la camminata inversa, poi trasformare una distanza sulla pagina in unità reali tramite /X (o /Y con /CYX) e il fattore /D iniziale. Il test dei bounds usa Min e Max perché una cornice di misura ruotata rispetto alla pagina può elencare i propri angoli in un altro ordine
uses
SysUtils, Math, HPDFDoc;
// Percorrete /VP all'indietro dall'ultima voce; vince il BBox contenitore raggiunto prima
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;
// Distanza fra due punti della pagina, nell'unità /D iniziale
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); // decide il punto di partenza
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;
Output atteso per il foglio scritto prima: Stair detail 1:20 (1 cm = 20 cm): 0.60 m, dato che 85 punti a 0.0070556 m per punto fanno 0.5997 m. Scambiate le due chiamate AddRectilinearViewportEx e la stessa linea da 85 punti si risolve sul viewport della planimetria e legge 3.00 m. Per un corredo di disegni scannerizzato che non avete prodotto voi, THotPDF.AddPageRectilinearViewportEx, AddPageGeospatialViewport e SetPageViewportPointDataCloud fanno lo stesso lavoro su un documento caricato, per indice di pagina, prima di SaveLoadedDocument. Le annotazioni line, polyline e polygon possono portare un /Measure proprio (ISO 32000-1 Tabelle 175 e 178), e GetLoadedAnnotationMeasure lo scompatta negli stessi record RL e GEO, cosa che conta quando le annotazioni di misura in un PDF revisionato vengono da un altro strumento
Con /D e /A fissati a 1, il fattore /X di 0.0352778 è l'unico numero che porta la scala 1:100, e il muro sud da 5 m dovrebbe leggere 5.00 m in qualunque reader che segua il §12.9. Lo stesso tipo di record torna da GetLoadedPageViewports, quindi un file scritto dal componente PDF HotPDF per Delphi può essere ricaricato e i suoi fattori confrontati con i valori che il vostro codice ci ha messo