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
/BaseStatede la configuration, où/ONet/Unchangedcomptent tous deux comme visibles et seul/OFFcache - 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
/Intentne contient ni/Viewni/All, puisqu'il ne prend aucune part à la visibilité d'intention de vue - Enfin exécuter le tableau
/ASde la configuration sélectionnée, dont les entrées pour l'événement correspondant fixent l'état des groupes qu'elles listent
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
À 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
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