Article technique

Calques PDF dans Delphi : Groupes de Contenu Facultatif (OCG)

Un géomètre ouvre un plan de site et souhaite que les courbes de niveau soient masquées tandis que les services publics restent affichés. Un réviseur souhaite que les annotations en rouge soient visibles à l'écran et disparaissent de l'impression. Une fiche produit est expédiée en trois langues à partir d'un seul fichier, et le lecteur choisit la langue affichée. Tous les trois sont la même fonctionnalité PDF, et le panneau qui les pilote dans Acrobat s'appelle Calques. La fonctionnalité sous ce panneau est le contenu facultatif, et c'est ce qui permet à une seule page de porter plusieurs strates visuelles indépendantes qu'un visualiseur active et désactive

Le contenu facultatif est spécifié dans la norme ISO 32000-1 §8.11. L'unité de visibilité est un groupe de contenu facultatif, un OCG, 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é. Une construction connexe, le dictionnaire d'appartenance de contenu facultatif ou OCMD, permet de faire dépendre la visibilité d'une combinaison booléenne de plusieurs groupes, mais le cas courant est un seul groupe nommé représentant un calque unique. Le document lie tout le mécanisme à travers une entrée de catalogue, /OCProperties, décrite ensuite

Ce que le catalogue doit contenir

Un OCG en lui-même est inerte. Pour qu'un visualiseur répertorie un calque et se souvienne de son état, le catalogue du document a besoin d'un dictionnaire /OCProperties, et le §8.11.4 définit exactement ce qu'il contient. Il y a un tableau /OCGs nommant chaque groupe dans le fichier, et il y a une entrée /D contenant la configuration par défaut. La configuration par défaut est la partie qu'un lecteur applique lors de la première ouverture du fichier. Elle enregistre les groupes qui démarrent activés et ceux qui démarrent désactivés, les entrées qui sont verrouillées contre le basculement par l'utilisateur, 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 dessiné sur la page, et il 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 peint et répertorié sans tenue de registres séparée de votre côté

Pourquoi un mode de conformité peut bloquer la fonctionnalité

Avant que le moindre code de calque ne s'exécute, la cible de conformité du document décide si le contenu facultatif est même légal. PDF/A-1, le profil d'archivage défini dans la norme ISO 19005-1, interdit carrément l'entrée /OCProperties dans son §6.1.13. Le raisonnement correspond à l'objectif du format. Un fichier d'archivage doit avoir un rendu identique pour chaque lecteur loin dans le futur, et un contenu dont un visualiseur peut modifier la visibilité est un contenu dont l'apparence n'est pas fixe, de sorte que le profil interdit la construction plutôt que de permettre une archive ambiguë. PDF/A-2 et PDF/A-3, définis dans ISO 19005-2 et ISO 19005-3, adoptent le point de vue opposé dans leur §6.9 et autorisent le contenu facultatif, avec des règles concernant la visibilité par défaut

Cette différence apparaît 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 requête produirait un fichier qui échoue à sa propre conformité déclarée. En mode PDF/A-2 ou PDF/A-3, et dans un PDF non contraint ordinaire, le même appel réussit et renvoie un ID de groupe non nul. Un résultat nul n'est donc pas un échec générique à inspecter plus tard ; c'est la bibliothèque qui vous indique que le niveau de conformité actif n'a pas de place pour la 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, pas un

Un calque n'est pas simplement visible ou invisible. La configuration par défaut enregistre son état à l'écran et un état d'impression séparé, car le §8.11.4 distingue ce qu'un visualiseur affiche de ce qu'un pipeline d'impression émet. Les deux sont indépendants à dessein. Un filigrane de brouillon peut être affiché à l'écran et retiré du papier, et un calque de ligne de coupe peut être masqué à l'écran tout en étant envoyé à un traceur. Fusionner les deux forcerait l'un à suivre l'autre et ferait perdre exactement le contrôle que la fonctionnalité est censée donner

PDFlibPas expose la paire via deux mutateurs (setters). SetOptionalContentGroupVisible prend l'ID de groupe et un indicateur, où un signifie visible et zéro signifie masqué, et régit l'état par défaut à l'écran. SetOptionalContentGroupPrintable prend l'ID de groupe et un indicateur indiquant si le calque est émis lors de l'impression du document. Les accesseurs (getters) correspondants, GetOptionalContentGroupVisible et GetOptionalContentGroupPrintable, renvoient chacun un ou zéro, vous permettant ainsi de relire la disposition d'écran et d'impression d'un calque séparément 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 fixe. Vous dessinez le contenu du calque sur la page courante, puis appelez SetContentStreamOptional avec l'ID du groupe, ce qui enveloppe le flux de contenu actuel de la page de sorte que tout ce qui a été dessiné jusqu'à présent appartient à ce groupe. Parce que l'appel capture tout ce qui se trouve sur le flux à ce moment-là, la discipline consiste à poser les marques d'un calque, à les assigner, puis seulement à démarrer le calque suivant. L'exemple ci-dessous place les services publics sur la première page et les annotations d'un réviseur sur une seconde page, définit l'état d'écran et d'impression de chaque calque, et 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 d'annotations est le cas qui mérite d'être noté. Il est affiché à l'écran afin qu'un réviseur voie la note, et son indicateur d'impression est à zéro de sorte qu'une impression du même fichier ne comporte aucun texte de révision. Cette asymétrie est tout l'intérêt de séparer les deux états

Relecture de la configuration

La lecture des calques est une promenade différente à travers la même structure. Après le chargement d'un fichier, GetOptionalContentConfigCount indique combien de dictionnaires de configuration le document contient ; la première configuration par défaut est l'ID de configuration 1. Dans une configuration, GetOptionalContentConfigOrderCount donne le nombre d'entrées dans l'arborescence d'ordre, et vous les indexez à partir de 1. Pour chaque entrée, GetOptionalContentConfigOrderItemLabel renvoie son texte d'affichage et GetOptionalContentConfigOrderItemLevel renvoie sa profondeur d'imbrication, de sorte qu'un plan de panneau avec des sous-calques en retrait sous des en-têtes peut être reconstruit textuellement

Chaque entrée a également un type. GetOptionalContentConfigOrderItemType distingue un véritable groupe de contenu facultatif d'une simple étiquette de texte qui n'existe que pour diriger une section de l'arborescence. Cette distinction est importante car les requêtes d'état par groupe n'ont de sens que pour de vrais groupes. Pour une entrée de groupe, GetOptionalContentConfigState indique si la configuration le démarre activé, désactivé, ou le laisse inchangé, et GetOptionalContentConfigLocked indique si l'utilisateur est empêché de le basculer. La boucle ci-dessous effectue le rendu de l'arborescence d'ordre 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 gardent cette boucle correcte. L'index d'ordre commence à un, de 1 au compte total, correspondant à la façon dont la bibliothèque numérote l'arborescence en interne. Et les appels par groupe ne s'exécutent que lorsque le type d'élément est un groupe, car une étiquette de texte est un en-tête avec un nom et un niveau mais aucun état activé, désactivé ou verrouillé à interroger. Ignorez cette garde et vous demandez à une étiquette un état qu'elle n'a pas

Où cela s'intègre-t-il

Les calques sont un mécanisme de présentation, le moteur doit donc les honorer sur chaque chemin qui effectue le rendu d'une page, et le côté rendu est couvert 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 destiné à l'auteur et un lecteur bénéficie d'un plan de calque structuré, ce qui renvoie au travail dans notre article sur les PDF balisés et la structure d'accessibilité. Les deux vont de pair avec les API de contenu facultatif décrites ici, qui sont fournies dans le cadre de la Bibliothèque PDF Delphi aux côtés des facilités de page, de texte, de police et de conformité abordées ailleurs sur ce blog