Un plan de rez-de-chaussée au 1:100 exporté en PDF s'ouvre dans Acrobat, et l'outil de distance rapporte le mur sud de 5 m comme 1.97 in, sa longueur sur papier. La pièce manquante est un viewport /VP dont le dictionnaire de mesure rectiligne (/RL) convertit les points en mètres — une chose qu'HotPDF écrit depuis Delphi, avec les points de contrôle géospatiaux (/GEO) pour les cartes
Ce que le viewport donne à l'outil de mesure
Un viewport est un rectangle sur la page qui apporte son propre système de coordonnées de mesure. L'entrée optionnelle /VP du dictionnaire de page (ISO 32000-1 Table 30, PDF 1.6) porte un tableau de dictionnaires /Type /Viewport, et chacun ne demande qu'un /BBox : /Name le libelle dans l'interface du viewer et /Measure fournit l'échelle (ISO 32000-1 §12.9, Table 260 ; le même dictionnaire est la Table 265 de l'ISO 32000-2). Une page sans viewport laisse les outils de mesure d'Acrobat à une échelle papier 1:1, si bien que les 1.97 in sont simplement 50 mm d'encre mesurés fidèlement. /BBox s'écrit en espace utilisateur par défaut, coin inférieur gauche puis supérieur droit, et cet ordre de coins fixe la direction des axes x et y de mesure, que la norme déclare indépendants du /Rotate de la page. Si un jour vous aplatissez le /Rotate à travers les boîtes d'une page, chaque rectangle /VP est une géométrie de plus qui doit suivre le contenu
Les viewports peuvent se chevaucher, et le §12.9 résout le chevauchement par l'ordre du tableau : un lecteur qui cherche le viewport sous un point part de la dernière entrée /VP, itère à rebours et stoppe à celle dont le /BBox contient le point. Un encart de détail d'escalier au 1:20 sur une feuille au 1:100 doit donc venir après le viewport de la feuille entière — accolé plus tôt, le grand rectangle gagne chaque test de frappe, et le détail se mesure cinq fois trop long. Les distances suivent leur point de départ. Une ligne qui commence dans l'encart et finit sur le plan principal se mesure au 1:20 sur toute sa longueur, bordure ou non
L'arithmétique derrière une échelle rectiligne
Le dictionnaire de mesure rectiligne (ISO 32000-1 Table 262, Table 267 dans ISO 32000-2) a quatre entrées requises, et toute son arithmétique pend à /C. /X, /D et /A sont des tableaux de formats de nombre : des dictionnaires de format de nombre (Table 263) ordonnés de la plus grande unité vers le bas, chacun avec un label /U et un facteur de conversion /C. Les dictionnaires suivants d'un tableau convertissent depuis l'unité qui les précède, tandis que le premier dictionnaire part d'un autre endroit selon l'entrée qui possède le tableau :
| Entrée | Requise | Le premier /C convertit depuis | Feuille métrique 1:100 |
|---|---|---|---|
| /R | Oui | Rien ; texte de ratio lisible par un humain | (1 cm = 1 m) |
| /X | Oui | Unités de l'espace utilisateur par défaut | /U (m) /C 0.0352778 |
| /Y | Quand les unités y ou les facteurs diffèrent | Unités de l'espace utilisateur par défaut | Omise |
| /D | Oui | L'unité /X de tête | /U (m) /C 1 |
| /A | Oui | L'unité /X de tête, au carré | /U (sq m) /C 1 |
| /T | Non | Degrés | Omise |
| /S | Non | Unité /Y de tête divisée par l'unité /X de tête | Omise |
Sur un plan métrique au 1:100, l'échelle tient dans un seul nombre. Un point fait 25.4 / 72 = 0.352778 mm de papier, qui représente 35.2778 mm de bâtiment, si bien que /X reçoit /U (m) et /C 0.0352778, tandis que /D part des mètres et /A des mètres carrés et que tous deux reçoivent /C 1. Copier le facteur /X dans /D et l'élever au carré pour /A applique l'échelle deux fois : le mur de 5 m revient comme 0.18 m, et une pièce de 20 m² rétrécit à 0.025 m². Écrivez ce /C de tête valant 1 dans /D et /A sur chaque viewport, même là où il semble redondant. La Table 262 rend les deux tableaux requis et ne définit aucun repli, si bien qu'un viewer qui rencontre un facteur manquant ou deviné improvise, et deux viewers peuvent improviser de deux façons différentes ; un dictionnaire écrit en entier épingle chaque affichage dans le fichier lui-même, et les unités se trouvent dans du code qu'un relecteur peut confronter au cartouche. Le reste du modèle de format de nombre marche ainsi :
- Les unités chaînées suivent la règle de l'unité précédente, c'est ainsi que l'exemple de la norme elle-même imprime "1 mi 2,378 ft 7 5/8 in" à partir de miles à /C 1, pieds à /C 5280 et pouces à /C 12, avec /F /D 8 arrondissant aux huitièmes
- /R est une légende pour les humains, si bien qu'une chaîne de ratio fausse sur un /C juste produit quand même des nombres justes
- Les profils à échelle verticale exagérée, le cas "in X 1 cm = 1 m, in Y 1 cm = 30 m" de la Table 262, demandent /Y plus un facteur /CYX qui convertit les unités y en unités x ; sans /CYX un lecteur peut refuser de calculer distance, aire et angle
Écrire un plan au 1:100 avec un encart de détail au 1:20
THPDFPage.AddRectilinearViewportEx prend le rectangle du viewport et un enregistrement THPDFRectilinearMeasureInfo dont les champs se mappent sur /R, /X, /Y, /D, /A, /T, /S, /O et /CYX, chaque tableau de formats de nombre étant porté comme enregistrements THPDFNumberFormatEntry. La table ci-dessus part telle quelle, et rien ne reste à inférer pour le lecteur. Les règles d'appel qui s'appliquent avant que le code ci-dessous ne tourne :
- Un viewport rectiligne demande PDF 1.6. HotPDF monte
THotPDF.Versionàpdf16à la demande ; avecStrictVersionLockactif, la méthode n'écrit rien et renvoie -1 - Un /R vide, ou un tableau /X, /D ou /A sans entrées, lève une exception, puisque la Table 262 exige les quatre
- Chaque appel accole à /VP, si bien que le plan part avant l'encart et que l'encart finit là où un lecteur le teste avant tout le reste
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 = centièmes
end;
// Dessin métrique au 1:Scale avec chaque affichage en mètres
function MetricScale(const Ratio: AnsiString;
Scale: Double): THPDFRectilinearMeasureInfo;
const
MetersPerPoint = 0.0254 / 72; // longueur papier d'une 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); // depuis les unités X
SetLength(Result.AreaFormats, 1);
Result.AreaFormats[0] := NumberFormat('sq m', 1, 100); // depuis les unités X au carré
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;
// ... dessiner le plan du rez-de-chaussée et l'encart du détail escalier ...
// Les rectangles sont en espace utilisateur PDF par défaut : points, origine bas-gauche
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;
Les quatre arguments du rectangle partent dans /BBox tels quels, en espace utilisateur par défaut PDF : des points, mesurés depuis le coin inférieur gauche. C'est l'espace depuis lequel le facteur /X convertit, donc rectangle et échelle partagent une unité. Les appels de dessin de HotPDF en utilisent une autre, avec l'origine en haut à gauche et des coordonnées en 1/Resolution de pouce, et l'écart entre les unités de dessin HotPDF et les points PDF s'applique ici de plein fouet — un encart dessiné en coordonnées haut-gauche demande ses valeurs y retournées contre la hauteur de page et converties en points, sinon le rectangle de mesure atterrit sur l'autre moitié de la feuille. Les pages grands formats ajoutent un facteur de plus. Avec /UserUnit fixé via THPDFPage.SetUserUnit, une unité par défaut vaut UserUnit / 72 de pouce, si bien que le facteur /X se met à l'échelle par la même valeur
Quel ordre les coordonnées GPTS utilisent-elles ?
Un viewport de carte porte un dictionnaire de mesure avec /Subtype /GEO, né comme extension Adobe de PDF 1.7 et devenu partie de la norme en PDF 2.0 (ISO 32000-2 §12.10.2, Table 269). /GCS est requis et porte un dictionnaire /Type /GEOGCS (Table 270) ou un dictionnaire /Type /PROJCS (Table 271), identifié par un code /EPSG ou une chaîne /WKT ; pour un système géographique, la norme permet l'un des deux et interdit les deux ensemble. Utilisez EPSG dès que le système de coordonnées a un code. Un entier ne peut pas être cassé par des échappements de chaîne, et les lecteurs le résolvent contre le même registre qu'utilisent les outils SIG, tandis que WKT reste pour les projections personnalisées qui n'ont pas de code. /DCS nomme optionnellement un autre système pour afficher les positions. /PDU liste les unités d'affichage préférées par nom — linéaires, aires et angulaires, comme KM, SQKM et DEG — et HotPDF ne l'écrit que quand les trois unités de THPDFGeoMeasureInfo sont remplies
Les paires /GPTS sont latitude puis longitude quand /GCS est géographique, et coordonnées est puis nord quand il est projeté. L'ordre bascule. Des paires x/y SIG fournies telles quelles dans /GPTS sortent justes pour une carte UTM et fausses pour WGS 84. EPSG:4326 lui-même met la latitude devant la longitude, GeoJSON et la plupart des APIs de cartes web font l'inverse, et une carte des rues de Zurich enregistrée en ordre web-map montre le résultat : 8.54° est se lit comme une latitude, 47.37° nord comme une longitude, et le curseur rapporte des positions près de la frontière Éthiopie–Somalie. /LPTS porte les points correspondants dans le carré unitaire du viewport, où (0, 0) est le coin inférieur gauche de /BBox et (1, 1) le supérieur droit, une paire par paire /GPTS ; des coordonnées de page collées là mettent les points de contrôle à des centaines de carrés de la carte. /Bounds, le trait de cadre (neatline) qui limite où la géoréférence est valide, utilise le même carré unitaire et prend par défaut [0 0 0 1 1 1 1 0]. HotPDF vérifie la forme avant d'écrire quoi que ce soit : /GPTS demande un compte pair d'au moins quatre nombres, et un /LPTS non vide doit égaler sa longueur
Attacher un nuage de points à un viewport de carte
Un viewport GEO peut aussi porter un dictionnaire de données de points (ISO 32000-2 §12.10.5, Table 272) : /Type /PtData, /Subtype /Cloud, un tableau /Names qui sert d'en-têtes de colonnes et un tableau /XPTS de lignes. LAT et LON en degrés et ALT en mètres sont des en-têtes prédéfinis, et la norme traite les lignes comme une collection non ordonnée. Structuralement, /XPTS s'imbrique tandis que /GPTS reste à plat : un tableau interne par point, dimensionné sur /Names, donc [[47.3769 8.5417 408] [47.3780 8.5400 410]]. THPDFPage.SetViewportPointDataCloud accepte quand même une seule liste à plat, la forme que produit un export CSV, et la découpe en lignes selon le compte de colonnes ; une valeur qui parse comme un nombre avec séparateur décimal à point devient un nombre PDF, tout le reste reste une chaîne, et un compte qui n'est pas multiple du compte de colonnes lève une exception. Les deux appels demandent PDF 2.0, et l'indice de viewport que renvoie AddGeospatialViewport est l'endroit où le nuage s'attache
procedure AddCityMap(Page: THPDFPage);
const
// Coins du trait de cadre, latitude avant longitude : NO, NE, SE, SO
CornerLatLon: array[0..7] of Double =
(47.40, 8.45, 47.40, 8.62, 47.33, 8.62, 47.33, 8.45);
// Les mêmes coins dans le carré unitaire du 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 reste vide
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 garde grossièrement le rapport 12.8 km x 7.8 km de la carte
Map := Page.AddGeospatialViewport('City center', 36, 252, 576, 576, Geo);
if Map < 0 then
raise Exception.Create('GEO viewports need PDF 2.0');
// Trois colonnes, donc chaque trois valeurs deviennent une ligne /XPTS
Page.SetViewportPointDataCloud(Map, ['LAT', 'LON', 'ALT'],
['47.3769', '8.5417', '408',
'47.3780', '8.5400', '410']);
end;
Relire les viewports et mesurer côté serveur
THotPDF.GetLoadedPageViewports renvoie un THPDFViewportInfoArray pour un indice de page à partir de zéro, un THPDFViewportInfo par entrée /VP dans l'ordre du fichier, avec le BBox, le Name, le MeasureSubtype, l'enregistrement Rectilinear ou Geospatial parsé et les lignes PtData éventuelles. Un dictionnaire de mesure qui omet /Subtype revient en RL, le défaut que la Table 261 de l'ISO 32000-1 assigne. Les deux fonctions ci-dessous reproduisent ce qu'un viewer fait avec un clic : trouver le viewport par le parcours à rebours, puis transformer une distance de page en unités réelles via /X (ou /Y avec /CYX) et le facteur /D de tête. Le test des bornes utilise Min et Max parce qu'un cadre de mesure tourné contre la page peut lister ses coins dans un autre ordre
uses
SysUtils, Math, HPDFDoc;
// Parcourt /VP à rebours depuis la dernière entrée ; le BBox contenant atteint le plus tôt gagne
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;
// Distance entre deux points de page, dans l'unité /D de tête
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); // le point de départ décide
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;
Sortie attendue pour la feuille écrite plus tôt : Stair detail 1:20 (1 cm = 20 cm): 0.60 m, puisque 85 points à 0.0070556 m par point font 0.5997 m. Échangez les deux appels AddRectilinearViewportEx et la même ligne de 85 points se résout vers le viewport du plan et se lit 3.00 m. Pour un jeu de dessins numérisés que vous n'avez pas produit, THotPDF.AddPageRectilinearViewportEx, AddPageGeospatialViewport et SetPageViewportPointDataCloud font le même travail sur un document chargé par indice de page avant SaveLoadedDocument. Les annotations ligne, polyligne et polygone peuvent porter leur propre /Measure (ISO 32000-1 Tables 175 et 178), et GetLoadedAnnotationMeasure le parse dans les mêmes enregistrements RL et GEO, ce qui compte quand les annotations de mesure d'un PDF relu viennent d'un autre outil
Avec /D et /A épinglés à 1, le facteur /X de 0.0352778 est le seul nombre qui porte l'échelle 1:100, et le mur sud de 5 m doit se lire 5.00 m dans tout lecteur qui suit le §12.9. Le même type d'enregistrement revient de GetLoadedPageViewports, si bien qu'un fichier écrit par le composant PDF Delphi HotPDF peut être rechargé et ses facteurs comparés aux valeurs que votre code y a mises