Un plan d'étage exporté par un paquet CAO d'architecture porte typiquement un tableau /VP sur sa première page avec deux viewports, et chaque viewport porte une entrée /Measure pour qu'un lecteur puisse lire des mètres directement sur le dessin. Une feuille de carte GeoPDF a le même tableau /VP et la même clé /Measure. Le plan d'étage est du PDF 1.6 ordinaire. La feuille de carte demande du PDF 1.7 plus Adobe Extension Level 3. Un contrôle de version qui s'arrête au nom de la clé exigera l'extension pour les deux, et le plan d'étage se fait refuser à la sauvegarde avec un message au sujet d'un contenu géospatial qu'il n'a jamais contenu
Une clé /Measure, deux dictionnaires différents
Le nom de la clé vous dit qu'un viewport est mesurable ; seul le dictionnaire derrière lui vous dit comment. Les dictionnaires Measure sont arrivés en PDF 1.6, et ISO 32000-1 §12.9 définit le genre rectiligne : une chaîne de rapport d'échelle dans /R, des tableaux de format de nombre dans /X et /Y, et d'autres tableaux pour la distance (/D) et la surface (/A). La mesure géospatiale est venue plus tard, dans Adobe Extension Level 3 au-dessus du PDF 1.7. Elle se marque par /Subtype /GEO et porte un système de coordonnées géographiques dans /GCS, un système de coordonnées d'affichage optionnel dans /DCS, des tableaux de points appariés dans /GPTS et /LPTS, et des unités d'affichage préférées dans /PDU — les structures parcourues dans la lecture des viewports GeoPDF et des tableaux GPTS et LPTS en Delphi
Le détail qui piège les gens, c'est le défaut. §12.9 donne à /Subtype une valeur par défaut RL, et pas mal d'exporteurs CAO en profitent pour omettre l'entrée. Un contrôleur qui lit un sous-type manquant comme "inconnu, prendre le pire" transforme chacun de ces dessins en document géospatial. Un contrôleur qui apparie sur la clé /Measure n'atteint même jamais la question du sous-type, car le dictionnaire de viewport qui porte la clé est un niveau au-dessus du dictionnaire de mesure qui y répond
Comment le preflight de version à la sauvegarde de PDF Library for Delphi rend son verdict
PDF Library for Delphi (PDFlibPas) fait tourner un preflight de version à l'intérieur de SaveToFile avant que toute sortie ne soit écrite. Il visite chaque objet indirect, en descendant dans les dictionnaires et tableaux imbriqués, et teste chaque nœud contre une table de règles de fonctionnalités. Chaque règle nomme une version PDF minimale et, pour les fonctionnalités d'extension, un niveau d'extension Adobe ; une règle d'extension ne peut être satisfaite que par du PDF 1.7 avec une entrée /Extensions /ADBE assortie dans le catalogue. La cible contre laquelle il vérifie est la version du document, ou la version que vous avez épinglée avec LockSaveVersion, plus le niveau d'extension que le catalogue déclare. La première règle qui correspond à la fois et dépasse cette cible met fin à la sauvegarde : SaveToFile renvoie 0 et LastErrorCode devient 602, la valeur de PDFLIB_ERROR_VERSION_COMPLIANCE
Un code d'erreur seul ne dit pas quoi corriger. Trois clés de GetInformation portent le reste : 311 est la version que la fonctionnalité en cause demande (par exemple 1.7 ExtensionLevel 3), 312 est son nom, et 313 est la version à laquelle la cible de sauvegarde est verrouillée, vide quand rien n'est verrouillé
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311 : version exigée, 312 : fonctionnalité qui l'a déclenchée,
// 313 : version à laquelle la cible de sauvegarde est verrouillée ('' si déverrouillée)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Pour le contenu de mesure la table porte trois règles et délibérément aucune règle sur la clé /Measure nue. Une page avec /VP ou /UserUnit demande du PDF 1.6. Un dictionnaire /PtData demande le niveau d'extension 3. Un dictionnaire de mesure demande le niveau d'extension 3 quand il dit /Subtype /GEO ou contient l'un de /GCS, /DCS, /GPTS, /LPTS ou /PDU. Ainsi un viewport dont le dictionnaire de mesure omet /Subtype et un qui épelle /RL passent tous deux à 1.6, tandis que la même page à 1.5 est refusée à cause de /VP. Ajoutez un tableau /GPTS et la réponse devient 1.7 ExtensionLevel 3
Le test de contenu est conservateur à dessein. Un dictionnaire rectiligne qui porte en plus un /GCS ou un /PDU égaré est traité comme géospatial. Nous préférons refuser ce fichier plutôt que le laisser passer : ces clés ne signifient rien dans le modèle rectiligne, si bien que qui les a écrites est soit un outil géospatial soit quelque chose qui a endommagé le fichier, et un lecteur conscient du GEO pourrait bien tenter de les interpréter. Le chargeur est tout aussi prudent dans l'autre sens. Son scan de fonctionnalités peut porter le numéro de version du document à 1.7, mais il n'écrit jamais de déclaration /Extensions au nom d'un fichier d'entrée — déclarer une extension est une affirmation sur le contenu, et seul votre code ou les appels d'écriture devraient la faire
Examiner les viewports d'un dessin avant de sauvegarder
Quand 312 nomme une fonctionnalité géospatiale et que vous êtes sûr que le fichier est un simple dessin, regardez vous-même les dictionnaires de mesure. Les appels publics de viewport suffisent. GetMeasureDictCoordinateSystem lit /Subtype et renvoie 1 pour RL, 2 pour GEO, et 0 quand l'entrée est absente — ce qui, au sens du §12.9, signifie aussi rectiligne. Les comptages de tableaux de points vous disent si des données de géoréférencement sont présentes
// Rapporte chaque dictionnaire de mesure du document chargé.
// Tous les appels ici ne font que lire ; aucun n'ajoute de clé au fichier.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = omis (RL), 1 = RL, 2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
Deux appels manquent à cette boucle à dessein : GetMeasureDictGCSDict et GetMeasureDictDCSDict. Ces deux-là sont des accesseurs obtenir-ou-créer bâtis pour l'écriture. Sur un dictionnaire de mesure sans entrée /GCS, GetMeasureDictGCSDict en ajoute une vide et déclare le niveau d'extension 3 pour le document. Appelez-le comme sonde sur un plan d'étage rectiligne et le dictionnaire contient désormais réellement /GCS ; une sauvegarde suivante verrouillée à 1.6 est refusée, correctement, pour du contenu que votre diagnostic vient d'écrire. Si vous devez savoir si un système de coordonnées existe, le sous-type et les comptages de tableaux de points y répondent sans effets de bord
Verrouiller la sortie à PDF 1.6
Une version épinglée garde la sortie honnête quand vos consommateurs ont un plafond. TPDFlib.LockSaveVersion accepte les chaînes '1.0' à '1.7' et renvoie 0 pour toute autre chose. Il fixe la version du document et empêche les appels d'écriture de la monter d'eux-mêmes, mais le preflight de sauvegarde tourne quand même contre la valeur verrouillée. Un dessin CAO verrouillé à 1.6 se sauvegarde proprement. Un vrai GeoPDF verrouillé à 1.6 reçoit 602, et c'est la réponse que vous voulez, car l'alternative est un fichier dont l'en-tête promet moins que ce que son contenu exige
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Contenu réel au-dessus de 1.6, par exemple un dictionnaire de mesure GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
finally
Pdf.UnlockSaveVersion;
end;
Lisez 311 et 312 avant de toucher à la version. Si la fonctionnalité est un vrai contenu géospatial, vous avez deux choix honnêtes : laisser le document déverrouillé à 1.7 avec l'extension déclarée, ou retirer les données géospatiales de la copie que vous envoyez à un consommateur limité au 1.6. Monter le verrou juste pour faire partir l'erreur est l'option à éviter. Elle cache la vraie question — le lecteur en aval sait-il traiter ce que vous expédiez ? Quand vous bâtissez du contenu géospatial via l'API, les setters SetMeasureDict* tels que SetMeasureDictCoordinateSystem et SetMeasureDictGPTSCount déclarent eux-mêmes le niveau d'extension 3, si bien que les fichiers écrits arrivent au preflight déjà étiquetés. La même habitude de contrôles bornés et adossés à des preuves se retrouve dans le preflight en mode auteur PDF/E-1 pour les documents d'ingénierie, où les dessins CAO rencontrent une norme de conformité plutôt qu'un numéro de version
Pourquoi le balayage de règles lit la table sur place
Le coût du preflight vaut à peu près nœuds d'objets fois règles, et le compte de nœuds inclut chaque nombre et chaque nom à l'intérieur de chaque tableau. Cela rend le surcoût par règle plus important qu'il n'y paraît. Chaque règle de la table est un enregistrement avec deux champs AnsiString, la chaîne d'appariement et le nom de fonctionnalité. Copier un tel enregistrement dans une variable locale fait que Delphi ou FPC incrémente deux compteurs de références et libère ce que la locale détenait avant. Fait une fois par règle et par nœud, cela signifie payer la comptabilité de chaînes même pour les règles qui ne s'appliquent pas à la version cible et sont jetées à la ligne suivante
La table de règles est remplie une fois à l'initialisation de l'unité et jamais modifiée ensuite, si bien que le balayage remet les entrées de table à son code d'appariement via des paramètres d'enregistrement const et les lit là où elles vivent. Un paramètre d'enregistrement const est passé par référence, si bien que tester une règle ne touche aucun champ de chaîne et les compteurs de références restent où ils étaient. L'effet se concentre sur le chemin d'appariement des règles. Une vraie sauvegarde décode aussi des objets, fait tourner la détection de fonctionnalités différée et sérialise la sortie, et ces étapes ne dépendent pas de la façon dont les règles sont passées. L'ordre des règles, les seuils et le diagnostic de premier échec en 311 et 312 n'en dépendent pas non plus, et aucun résultat de règle n'est mis en cache entre les sauvegardes, si bien qu'un document que vous éditez et sauvegardez deux fois est vérifié en entier les deux fois
Le même schéma mérite un contrôle dans votre propre code Delphi partout où une boucle parcourt une table d'enregistrements qui contiennent des chaînes, des interfaces ou des tableaux dynamiques :
type
TMyRule = record
Name: string; // champ managé : les copies coûtent du travail de comptage de références
Threshold: Integer;
end;
// Code du lecteur, montré comme un schéma
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const : pas de copie, pas de trafic de compteurs de références
end;
// Dans la boucle chaude, passez l'entrée de table directement au lieu de
// l'affecter d'abord à un enregistrement local
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
Viewports de mesure, dictionnaires géospatiaux, verrouillage de version et contrôle de conformité à la sauvegarde voyagent tous dans PDF Library for Delphi, disponible pour les projets Delphi, C++Builder et Lazarus