Article technique

Preflight de version PDF en Delphi : règles Measure RL/GEO

PDFlibPas (PDF Library for Delphi) vérifie chaque objet contre une table de règles de version PDF avant d'écrire un fichier, et jusqu'à récemment ce preflight de version PDF confondait les dictionnaires de mesure CAO ordinaires avec des dictionnaires géospatiaux. Un dessin CAO d'une seule page se chargeait bien, puis SaveToFile renvoyait 0 avec LastErrorCode 602 et exigeait 1.7 ExtensionLevel 3. Les règles corrigées traitent les dictionnaires /Measure rectilignes (/Subtype /RL) comme du PDF 1.6 banal et réservent le garde-fou d'extension aux vrais marqueurs géospatiaux

Le fichier est entré par admission de corpus : une page, un groupe optional-content, deux viewports de mesure rectilignes, le genre de sortie qu'écrit un paquet CAO d'architecture pour qu'un lecteur puisse lire des distances sur un plan d'étage. Rien d'exotique là-dedans, et c'est exactement pourquoi le refus comptait. Un preflight qui bloque un fichier valide est pire qu'un preflight lent, parce que l'appelant reçoit un diagnostic à l'allure autoritaire pointant une fonctionnalité que le document ne contient pas. Le correctif a pris deux morceaux : la lecture de la spécification derrière une règle, et le constat que la règle ne pouvait pas distinguer deux types de dictionnaires au niveau où elle regardait

Comment fonctionne le preflight de version à la sauvegarde dans PDFlibPas ?

Le garde-fou de sauvegarde, PrepareAndCheckSaveVersion, compare chaque objet indirect à PDFFeatureRules et échoue à la première règle qui correspond à la fois et qui exige plus que ce que la cible autorise. La cible, c'est la version du document (ou la version verrouillée par LockSaveVersion), plus le niveau d'extension Adobe déclaré sous /Extensions /ADBE. Chaque enregistrement TPDFFeatureRule porte un MinVersion, un MinExtensionLevel, un MatchKind comme fmkDictKey ou fmkDictSubtype, une chaîne Match, un nom de Feature lisible par un humain et un callback optionnel. AddRule enregistre une règle de version ordinaire ; AddExtensionRule épingle toujours MinVersion à 17 et ajoute par-dessus un niveau d'extension, donc une règle d'extension ne peut être satisfaite que par du PDF 1.7 plus la bonne entrée /Extensions. Quand le garde-fou se déclenche, la version exigée et le nom de la fonctionnalité sont gardés pour l'appelant, et les clés 311, 312 et 313 de GetInformation les exposent

Preflight de version à la sauvegarde dans PDFlibPas : PrepareAndCheckSaveVersion compare chaque objet aux enregistrements PDFFeatureRules portant MinVersion, un niveau d'extension et un match kind, AddExtensionRule épingle l'exigence à PDF 1.7 plus un niveau d'extension, et la première correspondance que la cible ne peut pas satisfaire arrête la sauvegarde avec erreur 602
Les clés 311, 312 et 313 de GetInformation transforment un refus en diagnostic, rapportant la version exigée, la fonctionnalité qui l'a déclenchée et la cible verrouillée, pour que l'appelant corrige le fichier ou la règle au lieu de deviner
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;

Pourquoi un simple dessin CAO échouait-il avec erreur 602 ?

La table de règles contenait AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), qui se déclenchait sur tout dictionnaire ayant simplement une clé /Measure, et chaque viewport de mesure en a une. Le tableau /VP de la page contient des dictionnaires de viewport, chaque viewport pointe son dictionnaire de mesure via /Measure, et la correspondance par présence de clé s'arrêtait là, sans regarder ce que le dictionnaire de mesure était réellement. Le balayage de fonctionnalités au chargement pouvait alors monter le numéro de version du document à 1.7, mais il n'écrit jamais une déclaration /Extensions au nom d'un fichier d'entrée, donc le garde-fou de sauvegarde voyait du PDF 1.7 au niveau d'extension 0 et rapportait 1.7 ExtensionLevel 3. Ce refus d'inventer une déclaration d'extension est délibéré : la bibliothèque ne promeut pas discrètement un fichier d'entrée pour maquiller une règle fausse

La spécification est sans ambiguïté sur le cas rectiligne. Les dictionnaires Measure sont arrivés avec PDF 1.6, et ISO 32000-1 §12.9 donne à /Subtype un défaut RL, un système de coordonnées rectiligne décrit par son propre jeu d'entrées : rapport d'échelle, formats numériques X et Y, distance et aire. La mesure géospatiale est l'ajout ultérieur de l'Adobe Extension Level 3 par-dessus PDF 1.7, identifié par /Subtype /GEO et porteur de tableaux de points géographiques, de dictionnaires de systèmes de coordonnées et d'unités d'affichage, les structures parcourues dans la lecture des viewports GeoPDF et des tableaux GPTS et LPTS en Delphi. Les deux dictionnaires pendent de la même clé /Measure, donc toute règle qui s'arrête à la clé ne peut pas être juste pour les deux. L'information discriminante se situe un niveau plus bas, dans le dictionnaire de mesure lui-même

Une clé /Measure, deux dictionnaires dans PDFlibPas : la mesure rectiligne, avec /RL ou sous-type omis, ne demande que du PDF 1.6, tandis qu'un dictionnaire géospatial demande 1.7 ExtensionLevel 3, donc CB_GeospatialDictionary décide au contenu là où l'ancienne règle de clé ne savait pas les distinguer
Un preflight qui bloque un fichier valide est pire qu'un preflight lent, parce que l'appelant reçoit un diagnostic autoritaire sur une fonctionnalité que le document n'a jamais contenue, et c'est pourquoi le contrôle discriminant est descendu d'un niveau

Qu'est-ce que le jeu de règles corrigé impose toujours ?

Le correctif supprime la règle de clé inconditionnelle et garde les garde-fous qui décrivent de vraies exigences de version. Une page portant /VP ou /UserUnit exige toujours PDF 1.6 via CB_PagePDF16Entries, une clé /PtData exige toujours le niveau d'extension 3, et CB_GeospatialDictionary décide si un dictionnaire de mesure est géospatial par son contenu plutôt que par la clé qui y a mené

// Retiré : tout dictionnaire avec une clé /Measure comptait comme géospatial
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);

AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);

function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
  Dict: TPDFDictionary;
begin
  Result := False;
  if not (Obj is TPDFDictionary) then
    Exit;
  Dict := TPDFDictionary(Obj);
  Result := (Dict.StringValue('Subtype') = 'GEO') or
    (Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
    (Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
    (Dict.FindIndexByKeyName('PDU') >= 0);
end;

Les régressions Delphi et FPC partagées épinglent cette frontière des deux côtés. Un viewport dont le dictionnaire de mesure omet /Subtype et un qui épelle /RL passent tous deux en PDF 1.6, la même page est toujours rejetée en PDF 1.5, et la détection de fonctionnalités ne rapporte plus d'extension pour elle. Ajouter un tableau /GPTS remet le verdict à 1.7 ExtensionLevel 3, qui passe une fois le niveau d'extension déclaré, et un dictionnaire nu /Subtype /GEO est refusé sans lui. Le callback est conservateur à dessein : un dictionnaire rectiligne qui porte aussi une clé égarée /GCS ou /PDU est traité comme géospatial, puisque ces clés n'ont pas de sens dans le modèle RL

LockSaveVersion est l'endroit où ce changement devient visible pour les appelants. TPDFlib.LockSaveVersion accepte '1.0' à '1.7', renvoie 0 pour toute autre chose, épingle la version du document et empêche les appels côté écrivain de la monter en silence, mais le garde-fou de sauvegarde court toujours contre la valeur verrouillée. Avec les règles corrigées, un fichier CAO verrouillé à 1.6 se sauvegarde proprement. Un vrai GeoPDF verrouillé à 1.6 prend toujours 602, ce qui est la bonne réponse, et les appels d'écriture géospatiale comme SetMeasureDictCoordinateSystem déclarent eux-mêmes le niveau d'extension 3 quand vous construisez ce contenu via l'API

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
  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))]);
end;
Pdf.UnlockSaveVersion;

Pourquoi le balayage des règles de version était-il plus lent que nécessaire ?

Le balayage copiait chaque TPDFFeatureRule dans un enregistrement local avant de le tester, et comme l'enregistrement porte deux champs AnsiString, chaque copie ajustait deux compteurs de références et libérait les valeurs précédentes. Le preflight visite chaque nœud de chaque arbre d'objets, scalaires compris, donc ce coût multipliait le nombre d'objets par le nombre de règles, et les règles qui ne s'appliquaient même pas à la version cible étaient copiées d'abord et sautées ensuite. Comme PDFFeatureRules est rempli une fois à l'initialisation de l'unité et traité en lecture seule, la v3.539.17 passe les entrées de table directement à MatchSingleRule et RuleExceedsTarget, dont les paramètres const Rule prennent une référence sans toucher aux chaînes

Accélération du balayage de règles dans PDFlibPas : le preflight copiait chaque enregistrement TPDFFeatureRule avant de le tester, en ajustant les compteurs de références AnsiString pour chaque objet visité, tandis que les paramètres const lisent désormais la table en lecture seule sur place, faisant passer la médiane d'une manche d'appariement de règles de 0,711 s à 0,203 s
Le gain est réel mais étroit : une sauvegarde complète paie aussi la détection de fonctionnalités différée, le décodage d'objets et la sérialisation, donc le ratio mesuré appartient au chemin d'appariement des règles et non au temps total de sauvegarde
// Avant : une copie d'enregistrement managé par règle, par objet visité
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// Après : les paramètres const lisent l'entrée de table immuable sur place
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
  Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
  RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
  FeatureName := PDFFeatureRules[X].Feature;
  Result := False;
  Exit;
end;

L'effet mesuré est étroit et doit être cité comme tel. Le benchmark confronte un tableau de 20 000 objets numériques à une cible PDF 1.4 dix fois par manche ; compilé avec FPC Win64 à -O2, la médiane de cinq manches est tombée de 0,711 s à 0,203 s, et faire tourner les deux builds en ordre inverse donnait 0,459 s contre 0,150 s. C'est environ un gain de 3x sur le seul chemin d'appariement des règles. Une vraie sauvegarde paie aussi la détection de fonctionnalités différée, le décodage d'objets et la sérialisation, donc le ratio ne se reporte pas sur le temps total de sauvegarde. L'ordre des règles, les callbacks, les seuils de version et le diagnostic du premier échec sont inchangés, et aucune règle n'a été mise en cache entre sauvegardes ni sautée pour y arriver

Que vérifier quand un PDF chargé échoue au preflight de version ?

Lisez les clés 311 et 312 avant de toucher à la version. Si la fonctionnalité nomme un dictionnaire géospatial et que le fichier ne dessine que des mesures rectilignes, c'était ce faux positif, et une build actuelle sauvegarde le fichier tel quel. Si la fonctionnalité est réelle, déclarez l'extension ou verrouillez vers une version qui contient honnêtement le contenu ; monter la version juste pour faire taire le garde-fou cache la question de savoir si les consommateurs en aval savent lire ce que vous livrez. Le même principe de contrôles bornés et adossés à des preuves anime 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

Les contrôles de conformité de version, les dictionnaires de mesure et géospatiaux, et le verrouillage de version de sauvegarde font tous partie de PDF Library for Delphi, la boîte à outils PDFlibPas pour développeurs Delphi, C++Builder et Lazarus