Un plano de planta baja 1:100 exportado a PDF se abre en Acrobat y la herramienta de distancia reporta el muro sur de 5 m como 1.97 in, su longitud en papel. La pieza que falta es un viewport /VP cuyo diccionario de medida rectilinear (/RL) convierte puntos a metros — algo que HotPDF escribe desde Delphi, junto con puntos de control geoespaciales (/GEO) para mapas
Qué le da un viewport a la herramienta de medida
Un viewport es un rectángulo de la página que trae su propio sistema de coordenadas de medida. La entrada opcional /VP del diccionario de página (ISO 32000-1 Tabla 30, PDF 1.6) guarda un arreglo de diccionarios /Type /Viewport, y cada uno necesita solo un /BBox: /Name lo rotula en la interfaz del visor y /Measure aporta la escala (ISO 32000-1 §12.9, Tabla 260; el mismo diccionario es la Tabla 265 en ISO 32000-2). Una página sin ningún viewport deja las herramientas de medida de Acrobat a escala de papel 1:1, así que los 1.97 in son simplemente 50 mm de tinta medidos con fidelidad. El /BBox se escribe en default user space, esquina inferior-izquierda y luego superior-derecha, y ese orden de esquinas fija la dirección de los ejes x e y de medida, que la norma declara independientes del /Rotate de la página. Si alguna vez aplana el /Rotate entre los boxes de una página, cada rectángulo /VP es una pieza más de geometría que debe seguir al contenido
Los viewports pueden solaparse, y la §12.9 resuelve el solapamiento por orden del arreglo: un lector que busca el viewport bajo un punto arranca en la última entrada /VP, itera hacia atrás y se detiene en la que tenga un /BBox que contenga el punto. Un detalle de escalera 1:20 insertado en una lámina 1:100 tiene por tanto que ir después del viewport de toda la lámina — añádalo antes y el rectángulo grande gana cada hit test, con lo cual el detalle mide cinco veces de más. Las distancias siguen su punto de partida: una línea que empieza dentro del detalle y termina en el plano principal se mide a 1:20 a lo largo de toda su extensión, con borde o sin él
La aritmética detrás de una escala rectilínea
El diccionario de medida rectilinear (ISO 32000-1 Tabla 262, Tabla 267 en ISO 32000-2) tiene cuatro entradas obligatorias, y toda su aritmética cuelga del /C. /X, /D y /A son arreglos de number format: diccionarios de formato de número (Tabla 263) ordenados desde la unidad mayor hacia abajo, cada uno con una etiqueta /U y un factor de conversión /C. Los diccionarios posteriores de un arreglo convierten desde la unidad que los precede, mientras que el diccionario inicial parte de otro lugar según qué entrada sea dueña del arreglo:
| Entrada | Obligatoria | El /C inicial convierte desde | Lámina métrica 1:100 |
|---|---|---|---|
| /R | Sí | Nada; texto de razón legible para personas | (1 cm = 1 m) |
| /X | Sí | Unidades del default user space | /U (m) /C 0.0352778 |
| /Y | Cuando las unidades o factores de y difieren | Unidades del default user space | Omitida |
| /D | Sí | La unidad /X inicial | /U (m) /C 1 |
| /A | Sí | La unidad /X inicial, al cuadrado | /U (sq m) /C 1 |
| /T | No | Grados | Omitida |
| /S | No | Unidad /Y inicial dividida por unidad /X inicial | Omitida |
En un plano métrico 1:100 la escala vive en un solo número. Un punto son 25.4 / 72 = 0.352778 mm de papel, que representan 35.2778 mm de edificio, así que /X recibe /U (m) y /C 0.0352778, mientras que /D parte de metros y /A de metros cuadrados, y ambos reciben /C 1. Copiar el factor de /X en /D y elevarlo al cuadrado para /A aplica la escala dos veces: el muro de 5 m vuelve como 0.18 m y un cuarto de 20 m² se encoge a 0.025 m². Escriba ese /C inicial de 1 en /D y /A en cada viewport, incluso donde parezca redundante. La Tabla 262 declara ambos arreglos obligatorios y no define ningún fallback, de modo que un visor que se topa con un factor faltante o inventado está improvisando, y dos visores pueden improvisar de dos maneras distintas; un diccionario escrito por completo fija cada lectura dentro del propio archivo, y las unidades quedan en código que un revisor puede cotejar con el cuadro de título. El resto del modelo de formato de número funciona así:
- Las unidades encadenadas siguen la regla de la unidad previa, que es como el propio ejemplo de la norma imprime "1 mi 2,378 ft 7 5/8 in" a partir de millas con /C 1, pies con /C 5280 y pulgadas con /C 12, con /F /F /D 8 redondeando a octavos
- El /R es un rótulo para personas, así que una cadena de razón equivocada sobre un /C correcto aun así produce números correctos
- Los perfiles con escala vertical exagerada, el caso "in X 1 cm = 1 m, in Y 1 cm = 30 m" de la Tabla 262, necesitan /Y más un factor /CYX que convierta unidades y en unidades x; sin /CYX un lector puede negarse a calcular distancia, área y ángulo
Escribir un plano 1:100 con un detalle insertado 1:20
THPDFPage.AddRectilinearViewportEx toma el rectángulo del viewport y un record THPDFRectilinearMeasureInfo cuyos campos se mapean a /R, /X, /Y, /D, /A, /T, /S, /O y /CYX, con cada arreglo de number format guardado como records THPDFNumberFormatEntry. La tabla de arriba va sin cambios, y no queda nada que el lector deba inferir. Las reglas de llamada que aplican antes de que el código de abajo corra:
- Un viewport rectilinear necesita PDF 1.6. HotPDF sube
THotPDF.Versionapdf16bajo demanda; conStrictVersionLockactivado, el método no escribe nada y devuelve -1 - Un /R vacío, o un arreglo /X, /D o /A sin entradas, lanza una excepción, porque la Tabla 262 exige los cuatro
- Cada llamada añade al /VP, así que el plano va antes del detalle y el detalle queda donde un lector lo prueba antes que todo lo demás
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 = centésimas
end;
// Plano métrico a 1:Scale con todas las lecturas en metros
function MetricScale(const Ratio: AnsiString;
Scale: Double): THPDFRectilinearMeasureInfo;
const
MetersPerPoint = 0.0254 / 72; // longitud en papel de una unidad
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); // desde unidades X
SetLength(Result.AreaFormats, 1);
Result.AreaFormats[0] := NumberFormat('sq m', 1, 100); // desde unidades X al cuadrado
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;
// ... dibuje el plano de la planta baja y el detalle de escalera ...
// Los rectángulos están en default user space del PDF: puntos, origen abajo-izquierda
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;
Los cuatro argumentos del rectángulo entran al /BBox tal cual, en default user space del PDF: puntos, medidos desde la esquina inferior-izquierda. Ese es el espacio desde el que convierte el factor /X, así que rectángulo y escala comparten una unidad. Las llamadas de dibujo de HotPDF usan otra, con el origen arriba a la izquierda y coordenadas en fracciones de 1/Resolution de pulgada, y la brecha entre unidades de dibujo de HotPDF y puntos PDF aplica aquí con toda su fuerza — un detalle dibujado en coordenadas de arriba-izquierda necesita sus valores y volteados contra la altura de la página y convertidos a puntos, o el rectángulo de medida cae en la mitad contraria de la lámina. Las páginas de formato grande añaden un factor más: con /UserUnit fijado vía THPDFPage.SetUserUnit, una unidad por defecto son UserUnit / 72 pulgadas, así que el factor /X se escala por el mismo valor
¿En qué orden van las coordenadas GPTS?
Un viewport de mapa lleva un diccionario de medida con /Subtype /GEO, que nació como extensión de Adobe para PDF 1.7 y pasó a ser parte del estándar en PDF 2.0 (ISO 32000-2 §12.10.2, Tabla 269). El /GCS es obligatorio y guarda un diccionario /Type /GEOGCS (Tabla 270) o un diccionario /Type /PROJCS (Tabla 271), identificados por un código /EPSG o una cadena /WKT; para un sistema geográfico la norma permite uno de los dos y prohíbe ambos juntos. Use EPSG siempre que el sistema de coordenadas tenga código: un entero no se puede romper con escape de cadenas, y los lectores lo resuelven contra el mismo registro que usan las herramientas GIS, mientras que WKT queda para las proyecciones a medida que no tienen código. El /DCS nombra opcionalmente otro sistema para mostrar posiciones. El /PDU lista unidades de visualización preferidas por nombre — lineales, de área y angulares, como KM, SQKM y DEG — y HotPDF solo lo escribe cuando las tres unidades de THPDFGeoMeasureInfo están llenas
Los pares /GPTS son latitud y luego longitud cuando el /GCS es geográfico, y este y luego norte cuando es proyectado. El orden se voltea. Pares x/y de GIS alimentando /GPTS directo salen bien para un mapa UTM y mal para WGS 84. EPSG:4326 en sí pone la latitud antes de la longitud, GeoJSON y la mayoría de las APIs de mapas web hacen lo contrario, y un mapa de calles de Zúrich registrado en orden de mapa web muestra el resultado: 8.54° este se lee como latitud, 47.37° norte como longitud, y el cursor reporta posiciones cerca de la frontera Etiopía–Somalia. El /LPTS guarda los puntos correspondientes en el cuadrado unitario del viewport, donde (0, 0) es la esquina inferior-izquierda del /BBox y (1, 1) la superior-derecha, un par por cada par /GPTS; coordenadas de página pegadas ahí dejan los puntos de control a cientos de cuadrados del mapa. El /Bounds, la neatline que limita dónde es válida la georreferencia, usa el mismo cuadrado unitario y por defecto es [0 0 0 1 1 1 1 0]. HotPDF revisa la forma antes de escribir nada: el /GPTS necesita un conteo par de al menos cuatro números, y un /LPTS no vacío debe igualar su longitud
Adjuntar una nube de puntos a un viewport de mapa
Un viewport GEO también puede cargar un diccionario de datos de puntos (ISO 32000-2 §12.10.5, Tabla 272): /Type /PtData, /Subtype /Cloud, un arreglo /Names que funciona como encabezados de columna y un arreglo /XPTS de filas. LAT y LON en grados y ALT en metros son encabezados predefinidos, y la norma trata las filas como una colección sin orden. Estructuralmente, /XPTS se anida mientras /GPTS queda plano: un arreglo interno por punto, dimensionado según /Names, o sea [[47.3769 8.5417 408] [47.3780 8.5400 410]]. THPDFPage.SetViewportPointDataCloud acepta igualmente una lista plana — la forma que produce un export CSV — y la rebana en filas según el conteo de columnas; un valor que parsea como número con punto decimal se vuelve un número PDF, cualquier otra cosa queda como cadena, y un conteo que no es múltiplo del conteo de columnas lanza una excepción. Ambas llamadas necesitan PDF 2.0, y el índice de viewport que devuelve AddGeospatialViewport es donde se engancha la nube
procedure AddCityMap(Page: THPDFPage);
const
// Esquinas de la neatline, latitud antes que longitud: 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);
// Las mismas esquinas en el cuadrado 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 queda vacío
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 a grandes rasgos el aspecto de 12.8 km x 7.8 km del mapa
Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
if Map < 0 then
raise Exception.Create('GEO viewports need PDF 2.0');
// Tres columnas, así que cada tres valores se vuelven una fila /XPTS
Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
['47.3769', '8.5417', '408',
'47.3780', '8.5400', '410']);
end;
Leer viewports y medir en el servidor
THotPDF.GetLoadedPageViewports devuelve un THPDFViewportInfoArray para un índice de página base cero, un THPDFViewportInfo por entrada /VP en orden de archivo, con el BBox, el Name, el MeasureSubtype, el record Rectilinear o Geospatial parseado y las filas PtData que haya. Un diccionario de medida que omite /Subtype vuelve como RL, el valor por defecto que asigna la Tabla 261 de ISO 32000-1. Las dos funciones de abajo reproducen lo que hace un visor con un clic: encontrar el viewport con el recorrido inverso y convertir una distancia de página a unidades reales vía /X (o /Y con /CYX) y el factor del /D inicial. La prueba de límites usa Min y Max porque un marco de medida rotado contra la página puede listar sus esquinas en otro orden
uses
SysUtils, Math, HPDFDoc;
// Recorra /VP hacia atrás desde la última entrada; gana el BBox contenedor alcanzado antes
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;
// Distancia entre dos puntos de página, en la unidad del /D inicial
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); // el punto de partida decide
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;
Salida esperada para la lámina escrita antes: Stair detail 1:20 (1 cm = 20 cm): 0.60 m, porque 85 puntos a 0.0070556 m por punto son 0.5997 m. Invierta las dos llamadas AddRectilinearViewportEx y la misma línea de 85 puntos resuelve al viewport del plano y lee 3.00 m. Para un juego de planos escaneados que usted no produjo, THotPDF.AddPageRectilinearViewportEx, AddPageGeospatialViewport y SetPageViewportPointDataCloud hacen el mismo trabajo sobre un documento cargado por índice de página antes de SaveLoadedDocument. Las anotaciones de línea, polilínea y polígono pueden llevar su propio /Measure (ISO 32000-1 Tablas 175 y 178), y GetLoadedAnnotationMeasure lo parsea en los mismos records RL y GEO, lo que importa cuando las anotaciones de medida en un PDF revisado vinieron de otra herramienta
Con /D y /A fijados en 1, el factor /X de 0.0352778 es el único número que porta la escala 1:100, y el muro sur de 5 m debería leer 5.00 m en cualquier lector que siga la §12.9. El mismo tipo de record vuelve de GetLoadedPageViewports, así que un archivo escrito por el componente PDF HotPDF para Delphi puede recargarse y sus factores compararse contra los valores que su código puso