Article technique

Basculer les calques optionnels PDF en Delphi via PDFium

PDFium Component pilote les calques de contenu optionnel PDF (OCG) en Delphi à travers deux méthodes de TPdf : InspectOptionalContent liste chaque calque avec la visibilité que PDFium rendra réellement, et SaveAsOptionalContentConfigured écrit une copie vérifiée dans laquelle les calques que vous choisissez sont activés ou désactivés. La deuxième méthode neutralise aussi les règles Usage et /AS qui autrement annuleraient discrètement votre modification. Les deux opèrent sur le document déjà ouvert dans TPdf, donc il n'y a pas de second analyseur à tenir en synchronisation avec ce que la visionneuse affiche

La demande vient en général d'un bureau d'études CAD ou SIG : le jeu de dessins embarque cotes, annotations et cartouche sur des calques séparés, et le client veut une copie sans les cotes avant envoi à un fournisseur. PDFium rend le contenu optionnel correctement, mais son ABI publique n'a aucune fonction pour énumérer les OCG, choisir une configuration ou basculer l'état d'un calque. Alors vous descendez au niveau objet, éditez /OCProperties, sauvegardez, rechargez, et le calque est toujours là. La raison tient à la logique de visibilité de PDFium, et elle mérite d'être comprise avant de toucher au moindre octet

Pourquoi éditer /ON et /OFF ne change-t-il pas ce que PDFium rend ?

Éditer les tableaux /ON et /OFF du dictionnaire de configuration ne suffit pas, parce que PDFium laisse un état explicite à l'intérieur du propre dictionnaire /Usage de l'OCG l'emporter sur ces tableaux, et une règle d'auto-état /AS peut ensuite passer outre les deux. L'ISO 32000-1 §8.11.4 décrit configurations et dictionnaires d'usage comme des mécanismes séparés ; le moteur de rendu de PDFium les fond dans une décision unique, et InspectOptionalContent la reproduit dans cet ordre :

  • Partir du /BaseState de la configuration, où /ON et /Unchanged comptent tous deux comme visibles et seul /OFF cache
  • Appliquer le tableau /ON de la configuration, puis son tableau /OFF, si bien qu'un groupe listé dans les deux finit caché
  • Appliquer l'état Usage explicite du groupe pour l'usage demandé, comme /Usage << /View << /ViewState /OFF >> >>, qui passe outre tout ce qui précède
  • Traiter comme visible un groupe dont /Intent ne contient ni /View ni /All, puisqu'il ne prend aucune part à la visibilité d'intention de vue
  • Enfin exécuter le tableau /AS de la configuration sélectionnée, dont les entrées pour l'événement correspondant fixent l'état des groupes qu'elles listent
La décision de visibilité en cinq étapes que PDFium Component rejoue pour chaque groupe de contenu optionnel PDF en Delphi : BaseState fixe le départ, les tableaux ON et OFF de la configuration s'appliquent dans l'ordre, une entrée Usage ViewState ou PrintState explicite passe outre les deux, la non-participation d'Intent compte comme visible, et le tableau AS tourne en dernier
Éditer les tableaux ON et OFF ne suffit pas parce que PDFium fond BaseState, les deux tableaux, l'état Usage du groupe et enfin les règles d'auto-état AS en un seul verdict qu'InspectOptionalContent reproduit pas à pas

La troisième étape est celle qui brûle les gens. Un fichier sauvegardé par un outil de mise en page porte souvent /ViewState /ON sur chaque OCG, et PDFium ignore alors votre tableau /OFF soigneusement édité : la sauvegarde réussit, le fichier se rouvre proprement, et le calque se peint toujours. Pour Print et Export, OcExplicitUsageState lit PrintState ou ExportState en premier et retombe sur ViewState quand l'entrée spécifique est absente, si bien qu'un ViewState /ON isolé épingle le calque pour l'impression aussi. Le contenu marqué qui référence un OCMD (§8.11.2.2) est ensuite résolu contre ces résultats par groupe, via la politique /P ou, quand elle est présente, l'expression de visibilité /VE

Comment lister les calques que PDFium affichera réellement ?

TPdf.InspectOptionalContent renvoie un TPdfOptionalContentInventory dont le tableau Groups transporte le numéro d'objet de chaque OCG, son nom, ses intentions, les trois états Usage, la langue, la plage de zoom, le drapeau Locked, l'index de groupe radio et le EffectiveVisible calculé. La méthode fait d'abord sauvegarder par PDFium le document courant en mémoire, développe les object streams et balaie le résultat, si bien que les modifications faites plus tôt dans la session sont reflétées. L'index de configuration 0 est toujours le dictionnaire /D par défaut et les entrées de /Configs suivent à partir de l'index 1 ; l'argument par défaut de -1 sélectionne l'index 0. Un document sans /OCProperties fait renvoyer à la méthode False avec la raison dans ErrorMessage plutôt que de lever une exception

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage vaut ocuView par défaut ; -1 sélectionne la configuration 0, le dictionnaire /D
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

Le tableau Memberships rapporte chaque OCMD avec sa Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), le texte brut de VisibilityExpression et son propre EffectiveVisible. Quelques règles de bord sont délibérées. /P vaut /AnyOn par défaut, et un OCMD sans groupes compte comme visible. Une référence à un numéro d'objet qui n'est pas un OCG connu est traitée comme visible plutôt que de faire échouer toute l'expression. L'évaluation de /VE s'arrête à une profondeur d'imbrication de 32 et traite tout ce qui est plus profond comme caché, ce qui empêche une expression hostile ou autoréférente de transformer l'inspection en débordement de pile

Écrire un nouvel état de calque avec SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured prend un tableau d'enregistrements TPdfOptionalContentStateChange (numéro d'objet du groupe plus Visible) et écrit un document dans lequel la configuration sélectionnée produit exactement cet état. La configuration sélectionnée reçoit /BaseState /ON plus des tableaux /ON et /OFF complets couvrant chaque groupe, et chaque OCG qui possède déjà un dictionnaire Usage reçoit un ViewState explicite (ou PrintState / ExportState, suivant Options.Usage) correspondant à son nouvel état. Avec TPdfOptionalContentConfigureOptions.Default, la clé /AS de la configuration sélectionnée est retirée pour qu'un événement d'ouverture, d'impression ou d'export ne puisse pas rebasculer les calques

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  SetLength(Changes, 2);
  Changes[0].GroupObjectNumber := DimensionsObj;
  Changes[0].Visible := False;
  Changes[1].GroupObjectNumber := NotesObj;
  Changes[1].Visible := True;

  // Configuration 0, ocuView, DisableAutomaticState et EnforceRadioGroups True
  Options := TPdfOptionalContentConfigureOptions.Default;

  if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
    Changes, Options, Report) then
    raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);

  Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
    [Report.ChangedGroupCount, Report.GroupCount,
     Report.UpdatedUsageStateCount,
     BoolToStr(Report.RemovedAutomaticState, True)]));
end;

Le chemin d'écriture garde la sortie sauvegardée de PDFium elle-même comme préfixe octet pour octet et n'ajoute que le propriétaire de configuration réécrit et les objets OCG qui portent des dictionnaires Usage, suivis d'une nouvelle section xref et d'un trailer. Avant qu'un seul octet n'atteigne votre destination, le résultat est rouvert dans un TPdf séparé sous la politique de chargement stricte, et la méthode échoue si la table de références croisées ne valide pas. La surcharge fichier va un pas plus loin : elle écrit dans un fichier temporaire à côté de la cible et ne remplace la cible qu'après réussite de la vérification, si bien qu'une mise à jour rejetée ne laisse jamais derrière elle un dessin à moitié écrit. C'est la même approche de révision incrémentale vérifiée qu'utilise l'éditeur d'arbres de noms et de numéros PDF de PDFium Component

Comment SaveAsOptionalContentConfigured dans PDFium Component écrit un PDF Delphi avec calques basculés : les changements d'état et les options entrent, la configuration sélectionnée est réécrite avec des tableaux ON et OFF complets et des états Usage, la révision incrémentale vérifiée est ajoutée, et une réouverture stricte doit valider avant que quoi que ce soit soit écrit
La sauvegarde configurée garde la resauvegarde de PDFium comme préfixe d'octets, ajoute le propriétaire de configuration réécrit plus une nouvelle section xref, et rouvre le résultat dans un TPdf séparé avant que la destination ne soit touchée

À quoi la sauvegarde configurée refuse-t-elle de consentir ?

La sauvegarde configurée refuse tout changement que le document lui-même interdit ou ne peut pas représenter en sécurité, et chaque refus arrive avant que la destination ne soit touchée. Un numéro d'objet absent de /OCGs échoue carrément. Changer un groupe listé dans le tableau /Locked de la configuration échoue, bien que reprendre sa valeur courante soit permis. Avec EnforceRadioGroups actif, tout ensemble /RBGroups qui se retrouverait avec plus d'un membre visible est rejeté au lieu de basculer silencieusement les autres. Les documents chiffrés sont rejetés parce que des objets incrémentaux en clair ne peuvent pas porter le gestionnaire de sécurité actif. Les documents signés lèvent EPdfError sauf si vous passez AllowSignedDocument = True, puisque changer ce qu'une page affiche peut casser la couverture de signature ou une politique de certification

Les portes de refus que SaveAsOptionalContentConfigured applique dans PDFium Component avant d'écrire un PDF Delphi configuré : un numéro d'objet hors OCGs échoue, les groupes verrouillés échouent, les ensembles RBGroups avec plus d'un membre visible sont rejetés, les documents chiffrés ne peuvent pas porter d'objets incrémentaux en clair, et les fichiers signés exigent AllowSignedDocument
Chaque refus arrive avant que la destination ne soit touchée, et la raison de l'échec atterrit dans Report.ErrorMessage au lieu de laisser derrière elle un dessin à moitié écrit
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // écrit /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // première entrée de /Configs, pas /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument reste False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // fichier signé : rien n'est écrit dans Target
      Result := False;
    end;
  end;
end;

Connaissez les compromis avant de câbler cela dans un traitement par lots. La révision ajoutée se pose sur la resauvegarde complète de PDFium, pas sur les octets de votre fichier d'origine, et c'est exactement pourquoi une entrée signée exige un consentement explicite. La réécriture normalise aussi la configuration sélectionnée en /BaseState /ON, si bien qu'une base /Unchanged ou /OFF d'un auteur est remplacée par des tableaux explicites de même visibilité résultante. Retirer /AS supprime des astuces propres à l'impression comme un calque de filigrane qui n'apparaît que sur papier ; mettez DisableAutomaticState à False pour garder ces règles, en acceptant qu'elles puissent passer outre l'état demandé pour cet événement. Côté positif, PDF/A-2 (ISO 19005-2 clause 6.9) et PDF/UA (ISO 14289-1 clause 7.10) interdisent tous deux /AS dans les dictionnaires de configuration, si bien que la sortie par défaut élimine un problème que votre validation preflight PDF/A avec PDFium Component rapporterait sinon

Où le contrôle des calques se place dans une visionneuse PDF Delphi

Dans une visionneuse, le contrôle des calques est une liste à cocher alimentée par l'inventaire plus un rechargement du résultat sauvegardé. Remplissez la liste depuis Groups, désactivez les entrées Locked, traitez les membres partageant un RadioGroupIndex comme mutuellement exclusifs, et à l'application écrivez dans un TMemoryStream puis rechargez ce flux dans TPdf pour que la vue peigne le nouvel état. Le câblage entre TPdf et TPdfView est couvert dans la construction d'une visionneuse PDF riche en fonctionnalités avec PDFium VCL en Delphi. Licences, téléchargements d'essai et le reste des fonctionnalités sont sur la page produit de PDFium Component pour Delphi