Uma planta de pavimento térreo 1:100 exportada para PDF abre no Acrobat, e a ferramenta de distância reporta a parede sul de 5 m como 1.97 in, o comprimento dela no papel. A peça que falta é um viewport /VP cujo dicionário de medida rectilinear (/RL) converte pontos em metros — algo que o HotPDF grava a partir do Delphi, junto com pontos de controle geoespaciais (/GEO) para mapas
O que um viewport dá à ferramenta de medida
Um viewport é um retângulo na página que traz o próprio sistema de coordenadas de medida dele. A entrada opcional /VP do dicionário de página (ISO 32000-1 Tabela 30, PDF 1.6) guarda um array de dicionários /Type /Viewport, e cada um precisa apenas de um /BBox: o /Name rotula ele na interface do viewer e o /Measure fornece a escala (ISO 32000-1 §12.9, Tabela 260; o mesmo dicionário é a Tabela 265 na ISO 32000-2). Uma página sem viewport nenhum deixa as ferramentas de medida do Acrobat na escala papel 1:1, então os 1.97 in são simplesmente 50 mm de tinta medidos com fidelidade. O /BBox é escrito em default user space, canto inferior esquerdo e depois superior direito, e essa ordem de cantos define a direção dos eixos x e y de medida, que a norma declara independentes do /Rotate da página. Se você um dia achatar o /Rotate entre as boxes de uma página, todo retângulo /VP é mais uma peça de geometria que precisa seguir o conteúdo
Viewports podem se sobrepor, e §12.9 resolve a sobreposição pela ordem do array: um reader procurando o viewport sob um ponto começa na última entrada /VP, itera para trás e para no primeiro cujo /BBox contém o ponto. Um recorte de escada 1:20 numa prancha 1:100 portanto precisa vir depois do viewport de prancha inteira — anexe-o antes e o retângulo grande ganha todo hit test, então o recorte mede cinco vezes demais. As distâncias seguem o ponto de partida delas. Uma linha que começa dentro do recorte e termina na planta principal é medida a 1:20 por todo o comprimento dela, com borda ou sem
A aritmética por trás de uma escala rectilinear
O dicionário de medida rectilinear (ISO 32000-1 Tabela 262, Tabela 267 na ISO 32000-2) tem quatro entradas exigidas, e toda a aritmética dele pende do /C. /X, /D e /A são number format arrays: dicionários de number format (Tabela 263) ordenados da maior unidade para baixo, cada um com um rótulo /U e um fator de conversão /C. Dicionários posteriores em um array convertem a partir da unidade anterior a eles, enquanto o dicionário líder parte de um lugar diferente dependendo de qual entrada é dona do array:
| Entrada | Exigida | O /C líder converte de | Prancha métrica 1:100 |
|---|---|---|---|
| /R | Sim | Nada; texto de razão legível por humanos | (1 cm = 1 m) |
| /X | Sim | Unidades de default user space | /U (m) /C 0.0352778 |
| /Y | Quando unidades ou fatores de y divergem | Unidades de default user space | Omitido |
| /D | Sim | A unidade líder do /X | /U (m) /C 1 |
| /A | Sim | A unidade líder do /X, ao quadrado | /U (sq m) /C 1 |
| /T | Não | Graus | Omitido |
| /S | Não | Unidade líder do /Y dividida pela unidade líder do /X | Omitido |
Numa prancha métrica 1:100 a escala mora em um número. Um ponto é 25.4 / 72 = 0.352778 mm de papel, que representam 35.2778 mm de edifício, então o /X recebe /U (m) e /C 0.0352778, enquanto o /D parte de metros e o /A de metros quadrados, e ambos recebem /C 1. Copiar o fator do /X para o /D e elevá-lo ao quadrado no /A aplica a escala duas vezes: a parede de 5 m volta como 0.18 m, e uma sala de 20 m² encolhe para 0.025 m². Escreva esse /C líder de 1 no /D e no /A em todo viewport, mesmo onde parece redundante. A Tabela 262 torna ambos os arrays exigidos e não define fallback nenhum, então um viewer que encontra um fator ausente ou chutado está improvisando, e dois viewers podem improvisar de dois jeitos diferentes; um dicionário escrito por completo fixa toda leitura no próprio arquivo, e as unidades ficam em código que um revisor pode conferir contra o carimbo. O resto do modelo de number format funciona assim:
- Unidades em cadeia seguem a regra da unidade anterior, que é como o exemplo da própria norma imprime "1 mi 2,378 ft 7 5/8 in" com milhas em /C 1, pés em /C 5280 e polegadas em /C 12, com /F /F /D 8 arredondando para oitavos
- O /R é uma legenda para pessoas, então uma string de razão errada sobre um /C certo ainda rende números certos
- Perfis com escala vertical exagerada, o caso "in X 1 cm = 1 m, in Y 1 cm = 30 m" da Tabela 262, precisam de um /Y mais um fator /CYX que converte unidades de y em unidades de x; sem /CYX um reader pode se recusar a calcular distância, área e ângulo
Escrevendo uma planta 1:100 com um recorte de detalhe 1:20
THPDFPage.AddRectilinearViewportEx recebe o retângulo do viewport e um record THPDFRectilinearMeasureInfo cujos campos mapeiam para /R, /X, /Y, /D, /A, /T, /S, /O e /CYX, com cada number format array guardado como records THPDFNumberFormatEntry. A tabela acima entra sem mudanças, e não sobra nada para o reader inferir. As regras de chamada que valem antes de o código abaixo rodar:
- Um viewport rectilinear precisa de PDF 1.6. O HotPDF eleva o
THotPDF.Versionparapdf16sob demanda; comStrictVersionLockativo, o método não escreve nada e retorna -1 - Um /R vazio, ou um array /X, /D ou /A sem entradas, levanta exceção, já que a Tabela 262 exige os quatro
- Cada chamada anexa ao /VP, então a planta entra antes do recorte e o recorte fica onde um reader o testa à frente de todo o 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 = centésimos
end;
// Desenho métrico a 1:Scale com toda leitura em metros
function MetricScale(const Ratio: AnsiString;
Scale: Double): THPDFRectilinearMeasureInfo;
const
MetersPerPoint = 0.0254 / 72; // comprimento em papel de uma unidade
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); // a partir de unidades X
SetLength(Result.AreaFormats, 1);
Result.AreaFormats[0] := NumberFormat('sq m', 1, 100); // a partir de unidades X ao quadrado
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;
// ... desenhe a planta do térreo e o recorte de detalhe da escada ...
// Retângulos são default user space do PDF: pontos, origem no canto inferior esquerdo
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;
Os quatro argumentos de retângulo vão para o /BBox como estão, em default user space do PDF: pontos, medidos do canto inferior esquerdo. Esse é o espaço de onde o fator /X converte, então retângulo e escala dividem uma unidade. As chamadas de desenho do HotPDF usam outra, com a origem no topo à esquerda e coordenadas em 1/Resolution de polegada, e a diferença entre unidades de desenho do HotPDF e pontos do PDF vale aqui por inteiro — um recorte desenhado em coordenadas de topo-esquerda precisa ter os valores de y invertidos contra a altura da página e convertidos para pontos, ou o retângulo de medida cai na metade oposta da prancha. Páginas de grande formato acrescentam um fator a mais. Com /UserUnit definido via THPDFPage.SetUserUnit, uma unidade default é UserUnit / 72 de polegada, então o fator /X escala pelo mesmo valor
Qual ordem as coordenadas GPTS usam?
Um viewport de mapa carrega um dicionário de medida com /Subtype /GEO, que começou como extensão Adobe do PDF 1.7 e virou parte da norma no PDF 2.0 (ISO 32000-2 §12.10.2, Tabela 269). O /GCS é exigido e guarda um dicionário /Type /GEOGCS (Tabela 270) ou um dicionário /Type /PROJCS (Tabela 271), identificado por um código /EPSG ou uma string /WKT; para um sistema geográfico a norma permite um dos dois e proíbe ambos juntos. Use EPSG sempre que o sistema de coordenadas tiver código. Um inteiro não pode ser quebrado por escaping de string, e os readers o resolvem contra o mesmo registry que as ferramentas GIS usam, enquanto o WKT fica para as projeções customizadas que não têm código. O /DCS opcionalmente nomeia outro sistema para exibir posições. O /PDU lista unidades de exibição preferidas pelo nome — lineares, de área e angulares, como KM, SQKM e DEG — e o HotPDF o grava só quando as três unidades do THPDFGeoMeasureInfo estão preenchidas
Pares /GPTS são latitude depois longitude quando o /GCS é geográfico, e easting depois northing quando é projetado. A ordem inverte. Pares x/y de GIS despejados direto no /GPTS saem certos para um mapa UTM e errados para WGS 84. O EPSG:4326 em si põe a latitude à frente da longitude, GeoJSON e a maioria das APIs de web map fazem o oposto, e um mapa de ruas de Zurique registrado em ordem de web map mostra o resultado: 8.54° leste é lido como latitude, 47.37° norte como longitude, e o cursor reporta posições perto da fronteira Etiópia–Somália. O /LPTS guarda os pontos correspondentes no quadrado unitário do viewport, onde (0, 0) é o canto inferior esquerdo do /BBox e (1, 1) o superior direito, um par por par de /GPTS; coordenadas de página coladas ali põem os pontos de controle a centenas de quadrados do mapa. O /Bounds, a neatline que limita onde a georreferência vale, usa o mesmo quadrado unitário e tem default [0 0 0 1 1 1 1 0]. O HotPDF confere a forma antes de gravar qualquer coisa: o /GPTS precisa de uma contagem par de pelo menos quatro números, e um /LPTS não vazio deve bater com o comprimento dele
Anexando uma nuvem de pontos a um viewport de mapa
Um viewport GEO também pode carregar um point data dictionary (ISO 32000-2 §12.10.5, Tabela 272): /Type /PtData, /Subtype /Cloud, um array /Names que funciona como cabeçalhos de coluna e um array /XPTS de linhas. LAT e LON em graus e ALT em metros são cabeçalhos predefinidos, e a norma trata as linhas como uma coleção sem ordem. Estruturalmente, o /XPTS aninha enquanto o /GPTS fica plano: um array interno por ponto, dimensionado pelo /Names, então [[47.3769 8.5417 408] [47.3780 8.5400 410]]. O THPDFPage.SetViewportPointDataCloud ainda aceita uma lista plana, a forma que um export CSV produz, e a fatia em linhas pela contagem de colunas; um valor que faz parse como número com separador decimal de ponto vira um número PDF, qualquer outra coisa fica string, e uma contagem que não é múltipla da contagem de colunas levanta exceção. Ambas as chamadas precisam de PDF 2.0, e o índice de viewport que o AddGeospatialViewport retorna é onde a nuvem se anexa
procedure AddCityMap(Page: THPDFPage);
const
// Cantos da neatline, latitude antes da longitude: 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);
// Os mesmos cantos no quadrado unitário do 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 segue vazio
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 mantém aproximadamente a proporção de 12.8 km x 7.8 km do mapa
Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
if Map < 0 then
raise Exception.Create('GEO viewports need PDF 2.0');
// Três colunas, então cada três valores viram uma linha /XPTS
Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
['47.3769', '8.5417', '408',
'47.3780', '8.5400', '410']);
end;
Lendo viewports de volta e medindo no servidor
O THotPDF.GetLoadedPageViewports retorna um THPDFViewportInfoArray para um índice de página base zero, um THPDFViewportInfo por entrada /VP em ordem de arquivo, com o BBox, o Name, o MeasureSubtype, o record Rectilinear ou Geospatial parseado e quaisquer linhas PtData. Um dicionário de medida que omite o /Subtype volta como RL, o default que a Tabela 261 da ISO 32000-1 atribui. As duas funções abaixo reproduzem o que um viewer faz com um clique: achar o viewport com o caminho reverso, depois converter uma distância de página em unidades reais pelo /X (ou /Y com /CYX) e o fator líder do /D. O teste de limites usa Min e Max porque um quadro de medida rotacionado contra a página pode listar os cantos em outra ordem
uses
SysUtils, Math, HPDFDoc;
// Percorra o /VP de trás para frente a partir da última entrada; o BBox contenedor alcançado primeiro ganha
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;
// Distância entre dois pontos de página, na unidade líder do /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); // o ponto 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;
Saída esperada para a prancha escrita antes: Stair detail 1:20 (1 cm = 20 cm): 0.60 m, já que 85 pontos a 0.0070556 m por ponto são 0.5997 m. Troque as duas chamadas AddRectilinearViewportEx de lugar e a mesma linha de 85 pontos resolve para o viewport da planta e lê 3.00 m. Para um conjunto de desenhos escaneados que você não produziu, o THotPDF.AddPageRectilinearViewportEx, o AddPageGeospatialViewport e o SetPageViewportPointDataCloud fazem o mesmo trabalho em um documento carregado por índice de página antes do SaveLoadedDocument. Annotations de linha, polilinha e polígono podem carregar o /Measure próprio delas (ISO 32000-1 Tabelas 175 e 178), e o GetLoadedAnnotationMeasure faz o parse para os mesmos records RL e GEO, o que importa quando as annotations de medida em um PDF revisado vieram de outra ferramenta
Com /D e /A fixados em 1, o fator /X de 0.0352778 é o único número que carrega a escala 1:100, e a parede sul de 5 m deve ler 5.00 m em qualquer reader que siga §12.9. O mesmo record type volta do GetLoadedPageViewports, então um arquivo escrito pelo componente Delphi PDF HotPDF pode ser recarregado e os fatores dele comparados com os valores que o seu código colocou lá