Article technique

Basculer les calques optionnels PDF en Delphi via PDFium

Un jeu de dessins arrive avec ses cotes sur un calque qui leur est propre, et la façon évidente de les cacher est de déplacer la référence de ce groupe du tableau /ON de la configuration par défaut vers /OFF. Le fichier s'enregistre et se rouvre, et les lignes de cote s'impriment toujours sur le cartouche. La cause siège à un objet de distance, à l'intérieur du dictionnaire du groupe : /Usage << /View << /ViewState /ON >> >>, un état explicite que le moteur de rendu lit avant les tableaux de configuration

L'état d'un calque peut résider à plusieurs endroits d'un PDF, et une édition de /OCProperties n'en touche qu'un. Le moteur PDFium de Google rend correctement le contenu optionnel, mais son API C publique n'a aucune fonction pour énumérer les groupes de contenu optionnel (OCG), choisir une configuration ou basculer un groupe. Du code Delphi qui pilote des calques doit réécrire les objets lui-même et rejouer exactement l'ordre de décision du moteur de rendu, sinon sa liste de calques rapporte un état tandis que la page en montre un autre

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

Les tableaux /ON et /OFF ne sont que la seconde de cinq étapes, et deux étapes ultérieures peuvent les passer outre. ISO 32000-1 §8.11.4.3 définit le dictionnaire de configuration de contenu optionnel avec /BaseState, /ON, /OFF, /Locked, /RBGroups et /AS, tandis que le §8.11.4.4 définit le dictionnaire Usage qu'un OCG peut porter. Dans la spécification, Usage décrit à quoi sert un groupe, et les entrées d'auto-état de /AS transforment cette description en ON ou OFF. PDFium prend un chemin plus court : quand il rend pour un usage, l'état explicite propre du groupe pour cet usage gagne. Repliés ensemble, l'ordre ressemble à ceci :

  • Partir du /BaseState de la configuration, où /ON et /Unchanged comptent tous deux comme visibles et seul /OFF cache
  • Appliquer le tableau /ON, puis le 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 explique le fichier d'ouverture : certains exporteurs écrivent /ViewState /ON dans chaque groupe qu'ils créent, et à partir de là aucune édition de tableaux ne cache ces calques. Impression et export suivent la même règle avec une torsion : le moteur de rendu lit /PrintState ou /ExportState d'abord et retombe sur /ViewState quand l'entrée spécifique manque. Un /ViewState /ON isolé épingle donc le calque sur papier aussi. La quatrième étape est plus discrète. Un groupe étiqueté /Intent /Design est hors de la visibilité d'intention de vue et se peint indépendamment des tableaux, tandis qu'un groupe sans clé /Intent est traité comme /View, le défaut que la spécification donne

Lister les calques et leur état rendu avec InspectOptionalContent

PDFium Component rejoue cet ordre dans TPdf.InspectOptionalContent. La méthode remplit un TPdfOptionalContentInventory et retourne True en cas de succès ; son paramètre Usage vaut ocuView par défaut et son paramètre ConfigurationIndex vaut -1, ce qui sélectionne l'index 0. L'index de configuration 0 est le dictionnaire /D par défaut, et les entrées de /Configs suivent à partir de l'index 1 dans l'ordre du fichier. Avant de balayer, la méthode fait sauvegarder par PDFium le document ouvert en mémoire et développe les object streams, si bien que les éditions faites plus tôt dans la session apparaissent dans le résultat. Un fichier sans /OCProperties, ou un index de configuration hors de la plage disponible, fait retourner False à l'appel avec la raison dans ErrorMessage ; aucune exception n'est levée dans les deux cas

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  Cfg: TPdfOptionalContentConfiguration;
  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;
  Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
  Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
    [Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
     BoolToStr(Cfg.HasAutomaticState, True)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %-24s visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

Chaque TPdfOptionalContentGroup porte son numéro d'objet, son nom, ses Intents et les trois états explicites ViewState, PrintState et ExportState, chacun parmi ocsUnspecified, ocsOn ou ocsOff. Quand un état lit ocsOn et que le groupe siège dans le tableau /OFF, vous avez trouvé le piège du paragraphe d'ouverture. Locked et RadioGroupIndex viennent de la configuration sélectionnée, et RadioGroupIndex vaut -1 pour un groupe qui n'appartient à aucun ensemble /RBGroups

Le reste du dictionnaire Usage est rapporté tel que le fichier le déclare : Language et LanguagePreferred, UserType et UserNames, PageElementSubtype, et MinimumZoom et MaximumZoom derrière un drapeau HasZoomRange. EffectiveVisible ne dépend d'aucun d'eux. L'affichage dépendant du zoom relève de votre visionneuse. Chaque TPdfOptionalContentConfiguration expose son nom, son BaseState, le contenu de ses tableaux et ses RadioGroups, plus OrderGroupObjectNumbers, qui aplatit /Order en références de groupes et saute les étiquettes textuelles des tableaux d'étiquettes imbriqués

L'impression et l'export peuvent diverger de l'écran

L'argument Usage change la réponse, et c'est le but de son existence. Sous ocuPrint, l'inventaire lit /PrintState avec son repli vers /ViewState, et seules les entrées /AS dont l'/Event est /Print participent. Un calque filigrane construit pour n'apparaître que sur papier se rapporte donc caché sous ocuView et visible sous ocuPrint. Comparer les deux passes trouve de tels calques avant qu'une impression ne surprenne quelqu'un

procedure TFormMain.ReportPrintOnlyLayers;
var
  OnScreen, OnPaper: TPdfOptionalContentInventory;
  I: Integer;
begin
  if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
     not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
    Exit;
  // Les deux passes lisent le même tableau /OCGs, donc l'ordre des groupes concorde
  for I := 0 to High(OnScreen.Groups) do
    if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
      Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
        [string(OnScreen.Groups[I].Name),
         BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
         BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;

Comment les expressions de visibilité OCMD sont-elles évaluées ?

Un dictionnaire d'adhésion à contenu optionnel (OCMD, ISO 32000-1 §8.11.2.2) lie un morceau de contenu à plusieurs groupes via une politique /P, ou via une expression de visibilité /VE qui combine des groupes avec /And, /Or et /Not. La spécification donne à /P un défaut de /AnyOn, laisse /VE primer sur /OCGs et /P quand elle est présente, et dit qu'un OCMD sans groupes n'a aucun effet sur la visibilité

Le tableau Memberships de l'inventaire suit ces règles contre les résultats par groupe de la même passe, si bien l'usage demandé se propage. Chaque adhésion rapporte les numéros d'objets de ses groupes, sa Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, ou ocmpUnknown pour un nom hors de ces quatre), le texte brut de VisibilityExpression et son propre EffectiveVisible. Un OCMD sans groupes compte comme visible. Une référence à un numéro d'objet qui n'est pas un OCG connu compte aussi comme visible, si bien qu'une référence pendante ne cache pas tout ce qui l'entoure. L'évaluation de /VE s'arrête à une profondeur d'imbrication de 32 et traite plus profond comme caché, si bien qu'une expression autoréférente ne peut pas faire déborder la pile. Le balayage liste chaque OCMD stocké comme objet indirect avec /Type /OCMD, object streams compris

Écrire un nouvel état de calque avec SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured prend un tableau d'enregistrements TPdfOptionalContentStateChange, chacun portant un GroupObjectNumber et un drapeau Visible. Il écrit une copie dans laquelle la configuration sélectionnée produit exactement cet état sous l'ordre en cinq étapes. Les groupes que vous ne mentionnez pas gardent la visibilité que l'inventaire a calculée pour eux

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
  ForPrint: Boolean);
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;

  Options := TPdfOptionalContentConfigureOptions.Default;
  if ForPrint then
  begin
    Options.Usage := ocuPrint;         // écrit /Print << /PrintState ... >>
    Options.ConfigurationIndex := 1;   // première entrée de /Configs, /D reste tel quel
  end;

  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;

La sortie garde le document sauvegardé de PDFium lui-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 que quoi que ce soit 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 ce chargement ne réussit pas ou si la table de références croisées ne valide pas. La surcharge fichier écrit dans un fichier temporaire à côté de la cible et ne remplace la cible qu'après vérification, si bien qu'une mise à jour rejetée laisse le dessin précédent intact. 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. Pour confirmer un préréglage d'impression, rechargez la sortie et appelez InspectOptionalContent(Inv, ocuPrint, 1) ; les groupes devraient rapporter les états que vous avez passés

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 interdit ou ne peut pas porter en sécurité, et chaque refus arrive avant que la destination ne soit écrite. Les contrôles tournent dans un ordre fixe. Un document signé lève EPdfError avant toute autre chose sauf si vous passez AllowSignedDocument = True. Un document chiffré est rejeté ensuite, parce que des objets incrémentaux en clair ne peuvent pas porter le gestionnaire de sécurité actif. Puis chaque changement demandé est vérifié : un numéro d'objet qui n'est pas dans /OCGs échoue carrément, et un changement sur un groupe listé dans le tableau /Locked de la configuration échoue, bien que reprendre sa valeur courante passe. Enfin, avec EnforceRadioGroups actif, un ensemble /RBGroups qui se retrouverait avec plus d'un membre visible est rejeté. Chaque refus sauf celui de la signature retourne False avec la raison dans Report.ErrorMessage

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 ; la raison atterrit dans Report.ErrorMessage, ou dans le message EPdfError quand le fichier est signé

Deux de ces portes vont plus loin que ce que la spécification exige strictement. Le §8.11.4.3 décrit /Locked comme une protection contre les changements via l'interface utilisateur de la visionneuse et décrit les groupes de boutons radio comme éteignant les autres membres quand l'un s'allume. PDFium Component applique les deux aussi aux changements programmatiques, et pour les groupes radio il refuse au lieu de choisir des membres à éteindre. Demander deux membres d'un ensemble mutuellement exclusif, la bibliothèque ne peut pas savoir lequel vous visiez, et une supposition contredirait votre demande. Éteignez les autres dans le même tableau Changes et la sauvegarde passe

La porte des documents signés existe parce que la révision ajoutée se pose sur la resauvegarde complète de PDFium, qui est une nouvelle séquence d'octets ; la plage d'octets qu'une signature existante couvre ne peut pas être supposée survivre. Ne passez AllowSignedDocument = True que quand perdre cette signature est acceptable

Trois effets de bord comptent dans un traitement par lots. La réécriture normalise la configuration sélectionnée en /BaseState /ON, si bien qu'une base /Unchanged ou /OFF d'un auteur devient des tableaux explicites de même visibilité résultante. Retirer /AS supprime aussi des astuces propres à l'impression comme un calque filigrane qui n'apparaît que sur papier ; mettez DisableAutomaticState à False pour garder ces règles et accepter qu'elles puissent passer outre votre état demandé pour leur événement. Et les tableaux /ON et /OFF sont écrits depuis les états calculés pour Options.Usage, si bien qu'un préréglage d'impression enregistré dans la configuration 0 change aussi ce que l'écran montre pour chaque groupe sans dictionnaire Usage. Gardez les préréglages d'impression dans une entrée /Configs et laissez /D à l'écran. Côté conformité, 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

Appliquer des changements de calques à l'intérieur d'une visionneuse Delphi

Dans une visionneuse, le contrôle des calques est une liste à cocher construite depuis l'inventaire, suivie d'une sauvegarde en mémoire et d'un rechargement. Remplissez la liste depuis Groups, désactivez les entrées dont le drapeau Locked est posé, traitez les membres partageant un RadioGroupIndex comme mutuellement exclusifs, et à l'application écrivez dans un TMemoryStream via la surcharge flux. TPdf.LoadDocument attend un composant inactif, donc éteignez-le d'abord ; le chargement bufferisé copie les octets, ce qui vous laisse libérer le flux tout de suite

procedure TFormMain.ApplyLayerChecklist(
  const Changes: TPdfOptionalContentStateChanges);
var
  Report: TPdfOptionalContentConfigureReport;
  Output: TMemoryStream;
  KeepPage: Integer;
begin
  Output := TMemoryStream.Create;
  try
    try
      if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
        TPdfOptionalContentConfigureOptions.Default, Report) then
      begin
        ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
        Exit;
      end;
    except
      on E: EPdfError do
      begin
        ShowMessage(E.Message);       // fichier signé : Output est encore vide
        Exit;
      end;
    end;
    KeepPage := Pdf.PageNumber;
    Pdf.Active := False;
    Pdf.LoadDocument(Output);         // Buffered vaut True par défaut
    Pdf.PageNumber := KeepPage;
  finally
    Output.Free;
  end;
end;

Après le rechargement, PDFium peint depuis la nouvelle configuration, si bien que l'écran et le prochain appel à InspectOptionalContent concordent. 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. Quand un document n'a pas encore de calques et qu'il faut les ajouter, créer des calques PDF avec OCG et OCMD en Delphi passe en revue SaveAsOptionalContentCreated

Tarifs, build d'essai et autres fonctionnalités documentaires du composant sont listés sur la page produit de PDFium Component pour Delphi