Technical Article

Calques PDF dans Delphi : Groupes de contenu optionnel (OCG)

Un géomètre ouvre un plan de situation et souhaite masquer les contours tandis que les réseaux restent visibles. Un réviseur veut que les annotations de correction soient visibles à l'écran et absentes de l'impression. Une fiche produit est fournie en trois langues dans un seul fichier, et le lecteur choisit la langue à afficher. Ces trois scénarios reposent sur la même fonctionnalité PDF, et le panneau qui les gère dans Acrobat s'appelle Calques. La fonctionnalité sous-jacente à ce panneau est le contenu optionnel, et c'est elle qui permet à une même page de porter plusieurs couches visuelles indépendantes qu'un visualiseur active et désactive.

Le contenu optionnel est spécifié dans la norme ISO 32000-1 §8.11. L'unité de visibilité est un groupe de contenu optionnel (OCG), c'est-à-dire un dictionnaire de type /OCG qui porte un nom. Le contenu marqué sur une page est associé à un groupe, et le visualiseur décide si ce groupe est actuellement affiché. Un concept connexe, le dictionnaire d'appartenance à un contenu optionnel (OCMD), permet de faire dépendre la visibilité d'une combinaison booléenne de plusieurs groupes, mais le cas quotidien est un groupe nommé unique représentant un calque unique. Le document relie l'ensemble du mécanisme par le biais d'une entrée de catalogue unique, /OCProperties, décrite ci-après.

Ce que le catalogue doit contenir

Un OCG en soi est inerte. Pour qu'un lecteur affiche un calque et mémorise son état, le catalogue du document nécessite un dictionnaire /OCProperties, et le §8.11.4 détaille précisément son contenu. Il contient un tableau /OCGs listant chaque groupe du fichier, et une entrée /D contenant la configuration par défaut. La configuration par défaut est la partie qu'un lecteur applique à l'ouverture initiale du fichier. Elle enregistre quels groupes démarrent activés et lesquels démarrent désactivés, quelles entrées sont verrouillées pour empêcher l'utilisateur de les basculer et, via un tableau /Order, comment les noms de calques sont organisés et imbriqués dans le panneau.

La conséquence pratique est que la création d'un calque n'est jamais un acte purement local. Le groupe doit être tracé sur la page et doit également être enregistré dans une structure au niveau du catalogue qui n'existait pas auparavant. PDFlibPas fait les deux pour vous. Le premier appel qui crée un groupe ajoute l'entrée /OCProperties au catalogue et initialise la configuration par défaut, de sorte que le calque est à la fois dessiné et répertorié sans gestion comptable distincte de votre part.

Pourquoi un mode de conformité peut interdire cette fonctionnalité

Avant que le moindre code de calque ne s'exécute, l'objectif de conformité du document décide si le contenu optionnel est autorisé. Le format PDF/A-1, le profil d'archivage défini dans l'ISO 19005-1, interdit formellement l'entrée /OCProperties dans son §6.1.13. Le raisonnement correspond à l'objectif du format. Un fichier d'archivage doit s'afficher de manière identique pour chaque lecteur à l'avenir, et un contenu dont le lecteur peut modifier la visibilité est un contenu dont l'apparence n'est pas figée ; le profil interdit donc cette structure plutôt que d'autoriser une archive ambiguë. Le PDF/A-2 et le PDF/A-3, définis dans l'ISO 19005-2 et l'ISO 19005-3, adoptent la position inverse dans leur §6.9 et autorisent le contenu optionnel, avec des règles sur la visibilité par défaut.

Cette différence se traduit directement dans l'API. Lorsque le document est en mode PDF/A-1, NewOptionalContentGroup refuse de créer le groupe et renvoie zéro, car honorer la demande produirait un fichier non conforme. En mode PDF/A-2 ou PDF/A-3, et en PDF standard sans contraintes, le même appel réussit et renvoie un identifiant de groupe non nul. Un résultat nul n'est donc pas une défaillance générique à inspecter plus tard ; c'est la bibliothèque qui vous indique que le niveau de conformité actif n'autorise pas cette fonctionnalité.

var
  Pdf: TPDFlib;
  LayerID: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);                       // PDF/A-1a: OCProperties forbidden

    LayerID := Pdf.NewOptionalContentGroup('Utilities');
    if LayerID = 0 then
      // refused under PDF/A-1; not a transient error, the mode bans layers
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

Deux états par calque, et non un seul

Un calque n'est pas simplement visible ou invisible. La configuration par défaut enregistre son état à l'écran et un état d'impression distinct, car le §8.11.4 distingue ce qu'un visualiseur affiche de ce qu'un flux d'impression produit. Les deux sont volontairement indépendants. Un filigrane de brouillon peut être affiché à l'écran et omis sur le papier, et un calque de lignes de coupe peut être masqué à l'écran tout en étant envoyé à un traceur. Fusionner les deux forcerait l'un à s'aligner sur l'autre, faisant perdre précisément le contrôle que cette fonctionnalité offre.

PDFlibPas expose cette paire à travers deux méthodes de définition. SetOptionalContentGroupVisible prend l'identifiant du groupe et un indicateur (un pour visible, zéro pour masqué) et régit l'état par défaut à l'écran. SetOptionalContentGroupPrintable prend l'identifiant du groupe et un indicateur spécifiant si le calque est produit lors de l'impression du document. Les méthodes de lecture correspondantes, GetOptionalContentGroupVisible et GetOptionalContentGroupPrintable, renvoient chacune un ou zéro, vous permettant de lire séparément la disposition écran et impression d'un calque plutôt que de déduire l'une de l'autre.

Construire deux calques sur une page

La création d'un calque et son remplissage suivent un ordre précis. Vous dessinez le contenu du calque sur la page actuelle, puis vous appelez SetContentStreamOptional avec l'identifiant du groupe, ce qui enveloppe le flux de contenu actuel de la page pour que tout ce qui a été dessiné jusqu'à présent appartienne à ce groupe. Comme l'appel capture ce qui se trouve sur le flux à cet instant précis, la discipline consiste à poser les éléments d'un calque, à les attribuer, puis seulement à commencer le calque suivant. L'exemple ci-dessous place les réseaux sur la première page et les corrections d'un réviseur sur une deuxième page, définit l'état écran et impression de chaque calque, puis enregistre.

var
  Pdf: TPDFlib;
  FontID, UtilLayer, RedlineLayer: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;                          // unconstrained PDF: layers allowed
    Pdf.SetPageDimensions(595, 842);          // A4 in points
    FontID := Pdf.AddStandardFont(0);         // Helvetica
    Pdf.SelectFont(FontID);

    // Layer 1: utilities, drawn then assigned to its own group
    Pdf.SetTextColor(0.10, 0.30, 0.65);
    Pdf.DrawText(72, 770, 'Utilities: water main, valve chamber');
    UtilLayer := Pdf.NewOptionalContentGroup('Utilities');
    Pdf.SetContentStreamOptional(UtilLayer);
    Pdf.SetOptionalContentGroupVisible(UtilLayer, 1);   // shown on screen
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper

    // Layer 2: reviewer redline on a fresh page
    Pdf.InsertPages(2, 1);                     // append one page after page 1
    Pdf.SetTextColor(0.80, 0.10, 0.10);
    Pdf.DrawText(72, 770, 'REVIEW: revise valve spec before issue');
    RedlineLayer := Pdf.NewOptionalContentGroup('Reviewer markup');
    Pdf.SetContentStreamOptional(RedlineLayer);
    Pdf.SetOptionalContentGroupVisible(RedlineLayer, 1);    // visible while reviewing
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // never printed

    Pdf.SaveToFile('SitePlan_Layers.pdf');
  finally
    Pdf.Free;
  end;
end;

Le calque de correction (redline) est le cas digne d'intérêt. Il est affiché à l'écran pour que le réviseur voie la note, et son indicateur d'impression est à zéro pour qu'une impression physique du même fichier ne contienne aucun texte de révision. Cette asymétrie est tout l'intérêt de séparer les deux états.

Lire la configuration en retour

La lecture des calques est un parcours différent à travers la même structure. Une fois le fichier chargé, GetOptionalContentConfigCount indique le nombre de dictionnaires de configuration que contient le document ; la première configuration par défaut porte l'identifiant 1. Dans une configuration, GetOptionalContentConfigOrderCount donne le nombre d'entrées dans l'arbre d'ordonnancement, que vous indexez à partir de 1. Pour chaque entrée, GetOptionalContentConfigOrderItemLabel renvoie son texte d'affichage et GetOptionalContentConfigOrderItemLevel renvoie sa profondeur d'imbrication, permettant de reconstruire textuellement l'arborescence d'un panneau avec des sous-calques indentés sous des rubriques.

Chaque entrée a également un type. GetOptionalContentConfigOrderItemType distingue un véritable groupe de contenu optionnel d'une simple étiquette de texte brut qui n'existe que pour titrer une section de l'arborescence. Cette distinction est importante car les requêtes d'état par groupe n'ont de sens que pour les vrais groupes. Pour une entrée de groupe, GetOptionalContentConfigState indique si la configuration l'active, le désactive ou le laisse inchangé au départ, et GetOptionalContentConfigLocked indique si l'utilisateur n'a pas le droit de le modifier. La boucle ci-dessous restitue l'arborescence avec l'état et le statut de verrouillage de chaque groupe, en indentant par niveau.

var
  Pdf: TPDFlib;
  Cfg, Count, I, ItemType, GroupID, Indent: Integer;
  Line: string;
begin
  Pdf := TPDFlib.Create(nil);
  try
    if Pdf.LoadFromFile('SitePlan_Layers.pdf', '') = 0 then Exit;
    if Pdf.GetOptionalContentConfigCount = 0 then Exit;

    Cfg := 1;                                  // the default configuration
    Count := Pdf.GetOptionalContentConfigOrderCount(Cfg);
    for I := 1 to Count do
    begin
      Indent := Pdf.GetOptionalContentConfigOrderItemLevel(Cfg, I);
      Line := StringOfChar(' ', Indent * 2)
              + Pdf.GetOptionalContentConfigOrderItemLabel(Cfg, I);

      ItemType := Pdf.GetOptionalContentConfigOrderItemType(Cfg, I);
      if ItemType = 1 then                     // 1 = optional content group
      begin
        GroupID := Pdf.GetOptionalContentConfigOrderItemID(Cfg, I);
        case Pdf.GetOptionalContentConfigState(Cfg, GroupID) of
          1: Line := Line + '  [on]';
          2: Line := Line + '  [off]';
          3: Line := Line + '  [unchanged]';
        end;
        if Pdf.GetOptionalContentConfigLocked(Cfg, GroupID) = 1 then
          Line := Line + ' (locked)';
      end;
      // ItemType = 2 is a text label heading; it has no per-group state

      Writeln(Line);
    end;
  finally
    Pdf.Free;
  end;
end;

Deux détails maintiennent cette boucle correcte. L'index d'ordonnancement est basé sur 1, de 1 au nombre d'éléments, correspondant à la numérotation interne de l'arbre par la bibliothèque. Et les appels par groupe ne s'exécutent que lorsque le type d'élément est un groupe, car une étiquette textuelle est un titre possédant un nom et un niveau mais aucun état d'activation, de désactivation ou de verrouillage à interroger. Omettez cette vérification et vous demanderez à une étiquette un état qu'elle ne possède pas.

Où cela s'intègre

Les calques sont un mécanisme de présentation, le moteur doit donc les respecter sur chaque chemin qui affiche une page, l'aspect rendu étant traité dans notre guide sur le rendu multi-moteur dans Delphi. Ils recoupent également la structure du document, car le nom d'un calque est un texte visible par l'auteur et un lecteur bénéficie d'un plan de calques structuré, ce qui renvoie au travail présenté dans notre article sur le PDF balisé et la structure d'accessibilité. Les deux s'associent aux API de contenu optionnel décrites ici, fournies avec la bibliothèque PDF pour Delphi en même temps que les outils de page, de texte, de police et de conformité présentés par ailleurs sur ce blog.