Le script complet dessine deux panneaux illustratifs et associe des données de mesure géospatiales à la page, à un Form XObject et à une image. Une région déclare un système géographique ; l’autre un système projeté. Ce sont deux régions de démonstration indépendantes, pas la conversion d’une même carte topographique.
Dessiner une grille ne crée pas une sémantique géographique. Le script construit séparément les systèmes de coordonnées, couples de points de contrôle, viewports de mesure, unités préférées et nuage de points qui décrivent les objets PDF.
Exécuter l’exemple
Depuis une copie du core 0.7.1 :
php examples/pdf20-geospatial.php geospatial.pdf
L’exemple fournit Courier avec des métriques explicites : FirstChar/LastChar, largeurs ASCII et FontDescriptor. PDF 2.0 ne permet plus d’omettre ces entrées pour les quatorze polices standard. Cette construction n’incorpore pas de programme de fonte ; un profil exigeant l’incorporation demande une fonte explicitement incorporée.
L’exemple utilise uniquement des objets PDF natifs. Il ne nécessite ni convertisseur d’images, ni convertisseur de fontes, ni base SIG, ni exécutable de projection. Un lecteur peut afficher la page sans proposer d’outils de mesure géographique ; l’aperçu montre bien le dessin réel.
Déclarer explicitement les systèmes de coordonnées
$geographic = new G\GeographicCoordinateSystem(new G\EpsgCode(4326));
$projected = new G\ProjectedCoordinateSystem(new G\EpsgCode(3857));
Ici, G est l’alias de PDF\Document\Geospatial dans le script. Un GeographicCoordinateSystem accepte exactement un code EPSG ou une chaîne WKT. Un ProjectedCoordinateSystem exige au moins l’un des deux. WKT est fourni par AsciiString ; la bibliothèque ne télécharge aucun registre, n’analyse pas toute la grammaire WKT et ne déduit pas une projection du dessin.
Modifier un identifiant ne recalcule pas les points de contrôle. Il faut fournir des coordonnées appartenant réellement au système choisi. GeographicPosition conserve la latitude avant la longitude ; ProjectedPosition conserve l’abscisse est avant l’ordonnée nord. Les valeurs doivent être finies et la latitude comprise entre −90 et 90 degrés. Aucune branche arbitraire de longitude n’est imposée.
Synchroniser les points locaux et réels
GeospatialControlPoint associe une LocalPosition à une position réelle. GeospatialControlPoints construit /LPTS et /GPTS à partir des mêmes couples, empêchant les longueurs différentes et les mélanges de dimensions ou de natures de coordonnées.
$point = new G\GeospatialControlPoint(
new G\LocalPosition(new N(0), new N(0)),
new G\GeographicPosition(new N(48.85), new N(2.34)),
);
L’exemple utilise quatre points de contrôle par région. Les coordonnées locales à deux dimensions appartiennent au carré unité de l’objet. GeospatialBounds fournit le polygone normalisé délimitant la zone utile ; il ne remplace pas le rectangle du viewport dans les coordonnées de la page. La variante 3D utilise des triplets dans les contextes 3D/RichMedia permis, avec une validation distincte plutôt qu’une conversion implicite de toutes les mesures en trois dimensions.
Donner son véritable propriétaire à chaque mesure
GeospatialMeasure écrit /Type /Measure, /Subtype /GEO, le système de coordonnées et les tableaux appariés. Un Viewport fournit le /BBox dans l’espace de la page, un nom facultatif et cette mesure ; Viewports fournit le tableau /VP de la page.
Lorsque des viewports se superposent, Viewports::forPoint() sélectionne le dernier contenant le premier point de la mesure. Viewport::local() convertit un point de page en coordonnées locales normalisées. Aucune de ces opérations n’effectue une projection géographique.
Le panneau gauche peint un Form XObject avec ses propres /Measure et /PtData ; celui de droite peint une image d’un pixel avec son propre /Measure. Leurs matrices de dessin positionnent l’illustration indépendamment des métadonnées géospatiales. Les contrôles du graphe effectif couvrent aussi les dictionnaires génériques et remplacements incrémentaux : ajouter les entrées manuellement ne contourne pas les règles de contexte ou de version.
Ajouter unités, matrice et données de points
PreferredDisplayUnits reçoit dans cet ordre une enum linéaire, une enum de surface et une enum angulaire. L’exemple choisit mètres, mètres carrés et degrés. Ces préférences décrivent l’affichage des mesures par un lecteur, pas les unités des opérateurs graphiques de la page.
ProjectedCoordinateSystemMatrix contient exactement douze coefficients numériques. Elle applique le vecteur ligne [x y z 1] avec la dernière colonne implicite [0 0 0 1] et prend priorité sur GPTS dans un système projeté. Sa méthode transform() réalise ce calcul affine ; elle ne convertit pas une projection cartographique en une autre.
PointDataNames nomme les colonnes du nuage ; chaque tuple PointData possède exactement cette largeur. Le tuple LAT/LON/ALT de démonstration contient 48.86, 2.35 et 35. PointDataCollection associe ces nuages à un viewport. Les données de colonnes et les opérateurs de dessin restent des objets indépendants.
Contrôler les relations sérialisées
tools/validate_pdf20_geospatial.py lit indépendamment GCS/GPTS/LPTS, Bounds, PDU, PCSM, viewports, propriétaires Form/image et nuages de points à l’aide de pypdf et qpdf. Il contrôle les sorties initiales, incrémentales et linéarisées dans les trois formats de références croisées, y compris leurs préfixes historiques et de repli.
python tools/validate_pdf20_geospatial.py \
--php php --qpdf qpdf --work-dir /tmp/pdf20-geospatial
Pour explorer le modèle, déplacez un viewport, changez ses unités préférées ou ajoutez un couple de points. Gardez les tuples synchronisés et choisissez explicitement la nature des coordonnées. Un outil SIG indépendant reste nécessaire pour établir la justesse de mesures réelles, du sens WKT ou d’une conversion de projection.
Références
Les paragraphes 12.9 et 12.10 d’ISO 32000-2:2020, tableaux 265–272, pages physiques 611–622, décrivent ces objets. L’original conservé contient les annotations EC3. Le profil natif consigne explicitement l’issue 358 comme Completed et les issues 533, 534 et 284 comme Accepted ; leurs autorités d’approbation distinctes restent visibles dans le catalogue.
Parcours des tutoriels · Guide du bundle · Couverture
Voir le résultat PDF
Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.
Utiliser le lecteur PDF du navigateur
Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».
Code source complet
Code de roukmoute/pdf · Consulter la source au commit documenté ↗
Lire le script commenté : pdf20-geospatial.php
<?php
declare(strict_types=1);
use PDF\Document\Geospatial as G;
use PDF\Document\Images\DeviceColorSpace;
use PDF\Document\Images\ImageDimensions;
use PDF\Document\Images\ImageSamples;
use PDF\Document\Images\SampleLayout;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Resources\XObject;
use PDF\FileStructure;
use PDF\Graphics\CoordinateSystems\Matrix;
use PDF\Graphics\CoordinateSystems\Point;
use PDF\Graphics\XObjects\FormXObject;
use PDF\Graphics\XObjects\ImageOptions;
use PDF\Graphics\XObjects\ImageXObject;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators as Op;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DataStructure\TextString;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Object\ArrayType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType as N;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// 1. Declare the reference system. The core stores EPSG/WKT explicitly; it does
// not download a registry or silently run a geographic projection engine.
$geographic = new G\GeographicCoordinateSystem(new G\EpsgCode(4326));
$projected = new G\ProjectedCoordinateSystem(new G\EpsgCode(3857));
$geographicPoints = [];
$projectedPoints = [];
foreach ([[0, 0, 48.85, 2.34, 1000, 2000], [1, 0, 48.85, 2.36, 2000, 2000], [1, 1, 48.87, 2.36, 2000, 3000], [0, 1, 48.87, 2.34, 1000, 3000]] as [$x, $y, $latitude, $longitude, $easting, $northing]) {
// GPTS latitude precedes longitude; projected GPTS uses easting/northing.
$local = new G\LocalPosition(new N($x), new N($y));
$geographicPoints[] = new G\GeospatialControlPoint($local, new G\GeographicPosition(new N($latitude), new N($longitude)));
$projectedPoints[] = new G\GeospatialControlPoint($local, new G\ProjectedPosition(new N($easting), new N($northing)));
}
$bounds = new G\GeospatialBounds(new Point(new N(0), new N(0)), new Point(new N(0), new N(1)), new Point(new N(1), new N(1)), new Point(new N(1), new N(0)));
$units = new G\PreferredDisplayUnits(G\LinearDisplayUnit::Metre, G\AreaDisplayUnit::SquareMetre, G\AngularDisplayUnit::Degree);
$geoMeasure = new G\GeospatialMeasure($geographic, new G\GeospatialControlPoints(...$geographicPoints), $bounds, displayUnits: $units);
// PCSM has twelve row-order coefficients, with [0 0 0 1] as its implicit final
// column. It takes priority over GPTS for the projected coordinate system.
$pcsm = new G\ProjectedCoordinateSystemMatrix(...array_map(static fn (int $value): N => new N($value), [1000,0,0,0,1000,0,0,0,1,1000,2000,0]));
$projectedMeasure = new G\GeospatialMeasure($projected, new G\GeospatialControlPoints(...$projectedPoints), $bounds, displayUnits: $units, matrix: $pcsm);
// 2. A point cloud retains its own column names and unordered tuples. These
// illustrative positions are data, independently of how the page is drawn.
$cloud = new G\PointData(new G\PointDataNames(new NameType('LAT'), new NameType('LON'), new NameType('ALT')), new ArrayType(new N(48.86), new N(2.35), new N(35)));
$clouds = new G\PointDataCollection($cloud);
$left = new Rectangle(new N(48), new N(350), new N(284), new N(620));
$right = new Rectangle(new N(310), new N(350), new N(546), new N(620));
$viewports = new G\Viewports(new G\Viewport($left, $geoMeasure, new TextString('Geographic illustration'), $clouds), new G\Viewport($right, $projectedMeasure, new TextString('Projected illustration')));
// 3. GEO measures also belong directly to form and image XObjects. Their LPTS
// use the object's unit square, independently of the page's drawing matrix.
$grid = [new Op\SetStrokingRGBColor(new N(0.15), new N(0.42), new N(0.55)), new Op\SetLineWidth(new N(0.004)), new Op\AppendRectangle(new N(0), new N(0), new N(1), new N(1)), new Op\StrokePath()];
foreach ([0.2,0.4,0.6,0.8] as $position) {
array_push($grid, new Op\MoveTo(new N($position), new N(0)), new Op\LineTo(new N($position), new N(1)), new Op\MoveTo(new N(0), new N($position)), new Op\LineTo(new N(1), new N($position)), new Op\StrokePath());
}
$form = new FormXObject(new ContentStreams(...$grid), new Rectangle(new N(0), new N(0), new N(1), new N(1)), new ResourceDictionary(), extensions: [new Entry(new NameType('Measure'), $geoMeasure), new Entry(new NameType('PtData'), $cloud)]);
$image = new ImageXObject(new ImageSamples(new ImageDimensions(1, 1), new SampleLayout(DeviceColorSpace::RGB), "\xe3\xe9\xd4"), options: new ImageOptions(extensions: [new Entry(new NameType('Measure'), $projectedMeasure)]));
// PDF 2.0 requires explicit metrics and a descriptor even for standard fonts.
$font = require __DIR__ . '/fixtures/pdf20-courier-font.php';
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))), new XObject(new DictionaryType(new Entry(new NameType('Grid'), new IndirectReference($form)), new Entry(new NameType('Raster'), new IndirectReference($image)))));
$operators = [];
foreach ([['PDF 2.0: geospatial objects',24,48,778], ['Explicit coordinate systems, point pairs and measurement viewports.',11,48,746], ['Geographic / EPSG 4326',12,48,648], ['Projected / EPSG 3857',12,310,648], ['Latitude, longitude',11,48,327], ['Easting, northing',11,310,327], ['GEO metadata is independent of the page illustration.',12,48,278], ['The left form and right raster carry their own GEO measures.',11,48,248], ['A LAT / LON / ALT cloud records the position 48.86, 2.35, 35.',11,48,223], ['PCSM records an explicit affine transformation for projected coordinates.',11,48,198], ['These independent sample regions are illustrative, not a surveyed map.',10,48,143], ['ISO 32000-2:2020, clauses 12.9 and 12.10; selected EC3 corrections.',10,48,118]] as [$text, $size, $x, $y]) {
array_push($operators, new Op\BeginTextObject(), new Op\SetTextFontAndSize(new NameType('F1'), new N($size)), new Op\MoveTextPosition(new N($x), new N($y)), new Op\ShowText(new LiteralString($text)), new Op\EndTextObject());
}
array_push($operators, new Op\SaveGraphicsState(), new Op\ConcatenateMatrix(new Matrix(new N(236), new N(0), new N(0), new N(270), new N(48), new N(350))), new Op\InvokeXObject(new NameType('Grid')), new Op\RestoreGraphicsState(), new Op\SaveGraphicsState(), new Op\ConcatenateMatrix(new Matrix(new N(236), new N(0), new N(0), new N(270), new N(310), new N(350))), new Op\InvokeXObject(new NameType('Raster')), new Op\RestoreGraphicsState());
$page = new PageObject(new Rectangle(new N(0), new N(0), new N(595), new N(842)), $resources, new StreamObject(new ContentStreams(...$operators)), new Entry(new NameType('VP'), $viewports));
$catalog = new DocumentCatalog((new PageTreeNode())->addKid($page));
$file = new FileStructure(new FileHeader('2.0'), new FileBody($catalog));
$path = $argv[1] ?? __DIR__ . '/pdf20-geospatial.pdf';
$file->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;
