Technischer Artikel

PDF-Optional-Content-Layer in Delphi mit PDFium umschalten

Ein Zeichnungssatz kommt mit seinen Bemaßungen auf einem eigenen Layer an, und der naheliegende Weg, sie zu verstecken, ist, die Referenz dieser Gruppe aus dem /ON-Array der Standardkonfiguration nach /OFF zu verschieben. Die Datei wird gespeichert und wieder geöffnet, und die Bemaßungslinien stehen weiterhin gedruckt über dem Schriftfeld. Die Ursache sitzt ein Objekt weiter, im Gruppenwörterbuch: /Usage << /View << /ViewState /ON >> >>, ein expliziter Zustand, den der Renderer vor den Konfigurations-Arrays liest

Der Zustand eines Layers kann an mehreren Orten in einer PDF leben, und eine Änderung an /OCProperties berührt nur einen davon. PDFium, Googles Engine, rendert Optional Content korrekt, aber seine öffentliche C-API hat keine Funktion, um Optional-Content-Gruppen (OCGs) aufzulisten, eine Konfiguration zu wählen oder eine Gruppe ein- und auszuschalten. Delphi-Code, der Layer steuert, muss die Objekte selbst umschreiben und die Entscheidungsreihenfolge des Renderers exakt nachspielen, sonst meldet seine Layerliste einen Zustand, während die Seite einen anderen zeigt

Warum ändert das Editieren von /ON und /OFF nichts daran, was PDFium rendert?

Die /ON- und /OFF-Arrays sind nur der zweite von fünf Schritten, und zwei spätere Schritte können sie überstimmen. ISO 32000-1 §8.11.4.3 definiert das Optional-Content-Konfigurationswörterbuch mit /BaseState, /ON, /OFF, /Locked, /RBGroups und /AS, während §8.11.4.4 das Usage-Wörterbuch definiert, das eine OCG tragen kann. In der Spezifikation beschreibt Usage, wozu eine Gruppe da ist, und die Auto-State-Einträge in /AS machen aus dieser Beschreibung ON oder OFF. PDFium nimmt den kürzeren Weg: Rendert es für einen Zweck, gewinnt der eigene explizite Zustand der Gruppe für diesen Zweck. Zusammengefasst sieht die Reihenfolge so aus:

  • Von /BaseState der Konfiguration ausgehen, wo /ON und /Unchanged beide als sichtbar zählen und nur /OFF verbirgt
  • Das /ON-Array anwenden, dann das /OFF-Array, sodass eine Gruppe, die in beiden steht, am Ende verborgen ist
  • Den expliziten Usage-Zustand der Gruppe für den angefragten Zweck anwenden, etwa /Usage << /View << /ViewState /OFF >> >>, der alles Vorstehende überstimmt
  • Eine Gruppe, deren /Intent weder /View noch /All enthält, als sichtbar behandeln, da sie an der View-Intent-Sichtbarkeit nicht teilnimmt
  • Das /AS-Array der gewählten Konfiguration zuletzt ausführen, wobei Einträge für das passende Event den Zustand der Gruppen setzen, die sie listen
Die fünfstufige Sichtbarkeitsentscheidung, die PDFium Component für jede PDF-Optional-Content-Gruppe in Delphi nachspielt: BaseState legt den Start fest, die ON- und OFF-Arrays der Konfiguration greifen der Reihe nach, ein expliziter Usage-ViewState- oder PrintState-Eintrag überstimmt beide, Intent-Nichtteilnahme zählt als sichtbar, und das AS-Array läuft zuletzt
ON- und OFF-Arrays zu editieren reicht nicht, weil PDFium BaseState, beide Arrays, den Usage-State der Gruppe und zuletzt die AS-Auto-State-Regeln zu einem Urteil zusammenfaltet, das InspectOptionalContent Schritt für Schritt nachbildet

Schritt drei erklärt die Eingangsdatei: Manche Exporteure schreiben /ViewState /ON in jede Gruppe, die sie anlegen, und von da an hilft kein Array-Editieren mehr, diese Layer zu verbergen. Drucken und Export folgen derselben Regel mit einer Wendung: Der Renderer liest zuerst /PrintState oder /ExportState und fällt auf /ViewState zurück, wenn der spezifische Eintrag fehlt. Ein einsames /ViewState /ON nagelt den Layer also auch aufs Papier. Schritt vier ist leiser. Eine Gruppe mit /Intent /Design steht komplett außerhalb der View-Intent-Sichtbarkeit und malt unabhängig von den Arrays, während eine Gruppe ohne /Intent-Schlüssel als /View behandelt wird — die Voreinstellung, die die Spezifikation vergibt

Layer und ihren gerenderten Zustand mit InspectOptionalContent auflisten

PDFium Component spielt diese Reihenfolge in TPdf.InspectOptionalContent nach. Die Methode füllt ein TPdfOptionalContentInventory und liefert True bei Erfolg; ihr Usage-Parameter hat die Voreinstellung ocuView, ihr ConfigurationIndex-Parameter -1, was Index 0 wählt. Konfigurationsindex 0 ist das Standard-/D-Wörterbuch, und die Einträge von /Configs folgen ab Index 1 in Dateireihenfolge. Vor dem Scan lässt die Methode PDFium das offene Dokument in den Speicher sichern und dehnt Objektstreams aus, sodass frühere Änderungen der Session im Ergebnis auftauchen. Eine Datei ohne /OCProperties oder ein Konfigurationsindex außerhalb des verfügbaren Bereichs lässt den Aufruf False mit dem Grund in ErrorMessage zurückgeben; eine Exception fliegt in beiden Fällen nicht

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  Cfg: TPdfOptionalContentConfiguration;
  G: TPdfOptionalContentGroup;
begin
  // Usage hat die Voreinstellung ocuView; -1 wählt Konfiguration 0, das /D-Wörterbuch
  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;

Jede TPdfOptionalContentGroup trägt ihre Objektnummer, ihren Namen, ihre Intents und die drei expliziten Zustände ViewState, PrintState und ExportState, jeweils einer von ocsUnspecified, ocsOn oder ocsOff. Liest ein Zustand ocsOn und die Gruppe steht im /OFF-Array, haben Sie die Falle aus dem Eingangsabsatz gefunden. Locked und RadioGroupIndex stammen aus der gewählten Konfiguration, und RadioGroupIndex ist -1 für eine Gruppe, die zu keinem /RBGroups-Set gehört

Der Rest des Usage-Wörterbuchs wird gemeldet, wie die Datei ihn angibt: Language und LanguagePreferred, UserType und UserNames, PageElementSubtype sowie MinimumZoom und MaximumZoom hinter einem HasZoomRange-Flag. EffectiveVisible hängt von keinem davon ab. Die zoomabhängige Anzeige ist Sache Ihres Viewers. Jede TPdfOptionalContentConfiguration legt ihren Namen, ihren BaseState, Arrayinhalte und RadioGroups offen, dazu OrderGroupObjectNumbers, das /Order zu Gruppenreferenzen abflacht und die Textlabels verschachtelter Label-Arrays überspringt

Druck und Export können vom Bildschirm abweichen

Das Usage-Argument ändert die Antwort, und genau dafür ist es da. Unter ocuPrint liest das Inventar /PrintState mit seinem Rückfall auf /ViewState, und nur /AS-Einträge, deren /Event /Print ist, nehmen teil. Eine Wasserzeichen-Ebene, gebaut, um nur auf Papier zu erscheinen, meldet sich unter ocuView also verborgen und unter ocuPrint sichtbar. Der Vergleich der beiden Durchgänge findet solche Layer, bevor ein Ausdruck jemanden überrascht

procedure TFormMain.ReportPrintOnlyLayers;
var
  OnScreen, OnPaper: TPdfOptionalContentInventory;
  I: Integer;
begin
  if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
     not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
    Exit;
  // Beide Durchgänge lesen dasselbe /OCGs-Array, also stimmt die Gruppenreihenfolge
  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;

Wie werden OCMD-Sichtbarkeitsausdrücke ausgewertet?

Ein Optional-Content-Membership-Dictionary (OCMD, ISO 32000-1 §8.11.2.2) bindet ein Inhaltselement über eine /P-Policy an mehrere Gruppen oder über einen /VE-Sichtbarkeitsausdruck, der Gruppen mit /And, /Or und /Not kombiniert. Die Spezifikation gibt /P die Voreinstellung /AnyOn, lässt /VE Vorrang vor /OCGs und /P haben, wenn es vorhanden ist, und sagt: Ein OCMD ohne Gruppen hat keinen Einfluss auf die Sichtbarkeit

Das Memberships-Array des Inventars folgt diesen Regeln gegen die Gruppenergebnisse desselben Durchgangs, sodass der angefragte Usage durchschlägt. Jede Membership meldet ihre Gruppenobjektnummern, ihre Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff oder ocmpUnknown für einen Namen außerhalb dieser vier), den rohen VisibilityExpression-Text und ihr eigenes EffectiveVisible. Ein OCMD ohne Gruppen zählt als sichtbar. Eine Referenz auf eine Objektnummer, die keine bekannte OCG ist, zählt ebenfalls als sichtbar, sodass eine einzige verlorene Referenz nicht alles um sich herum verbirgt. Die /VE-Auswertung stoppt bei einer Verschachtelungstiefe von 32 und behandelt alles Tiefere als verborgen, sodass ein selbstreferenzieller Ausdruck den Stack nicht überlaufen kann. Der Scan listet jeden OCMD, der als indirektes Objekt mit /Type /OCMD gespeichert ist, Objektstreams eingeschlossen

Einen neuen Layer-State mit SaveAsOptionalContentConfigured schreiben

TPdf.SaveAsOptionalContentConfigured nimmt ein Array von TPdfOptionalContentStateChange-Records entgegen, jeder mit einer GroupObjectNumber und einem Visible-Flag. Es schreibt eine Kopie, in der die gewählte Konfiguration unter der Fünfschritte-Ordnung exakt diesen Zustand erzeugt. Gruppen, die Sie nicht erwähnen, behalten die Sichtbarkeit, die das Inventar für sie berechnet hat

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;         // schreibt /Print << /PrintState ... >>
    Options.ConfigurationIndex := 1;   // erster /Configs-Eintrag, /D bleibt, wie es war
  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;

Die Ausgabe behält PDFiums eigenes gesichertes Dokument als byteidentisches Präfix und hängt nur den neu geschriebenen Konfigurations-Owner und die OCG-Objekte mit Usage-Wörterbüchern an, gefolgt von einer neuen Xref-Section und einem Trailer. Bevor irgendetwas Ihr Ziel erreicht, wird das Ergebnis in einem separaten TPdf unter der strikten Ladepolicy wieder geöffnet, und die Methode scheitert, wenn dieses Laden nicht gelingt oder die Cross-Reference-Tabelle nicht validiert. Die Datei-Überladung schreibt in eine temporäre Datei neben dem Ziel und ersetzt das Ziel erst nach der Verifikation, sodass eine abgelehnte Änderung die vorherige Zeichnung unberührt lässt. Es ist derselbe verifizierte Inkremental-Revision-Ansatz wie beim PDF-Name-Tree- und Number-Tree-Editor in PDFium Component. Um ein Druck-Preset zu bestätigen, laden Sie die Ausgabe neu und rufen InspectOptionalContent(Inv, ocuPrint, 1) auf; die Gruppen sollten die übergebenen Zustände melden

Wie SaveAsOptionalContentConfigured in PDFium Component ein Delphi-PDF mit umgeschalteten Layern schreibt: State-Änderungen und Optionen gehen hinein, die gewählte Konfiguration wird mit vollständigen ON- und OFF-Arrays und Usage-States neu geschrieben, die verifizierte Inkremental-Revision wird angehängt, und ein striktes Wiederöffnen muss validieren, bevor irgendetwas geschrieben wird
Der konfigurierte Save hält PDFiums eigenen Re-Save als Byte-Präfix, hängt den neu geschriebenen Konfigurations-Owner plus eine neue Xref-Section an und öffnet das Ergebnis in einem separaten TPdf wieder, bevor das Ziel angefasst wird

Was verweigert der konfigurierte Save?

Der konfigurierte Save verweigert jede Änderung, die das Dokument verbietet oder nicht sicher tragen kann, und jede Verweigerung passiert, bevor das Ziel geschrieben wird. Die Prüfungen laufen in fester Reihenfolge. Ein signiertes Dokument wirft EPdfError, bevor irgendetwas anderes geschieht, außer Sie übergeben AllowSignedDocument = True. Ein verschlüsseltes Dokument wird als Nächstes zurückgewiesen, denn Klartext-Inkremental-Objekte können den aktiven Security Handler nicht tragen. Dann wird jede angefragte Änderung geprüft: Eine Objektnummer, die nicht in /OCGs steht, scheitert rundweg, und eine Änderung an einer Gruppe im /Locked-Array der Konfiguration scheitert, obwohl das Wiederholen ihres aktuellen Werts durchgeht. Zuletzt, mit EnforceRadioGroups, wird ein /RBGroups-Set zurückgewiesen, das mit mehr als einem sichtbaren Member enden würde. Jede Verweigerung außer der Signatur liefert False mit dem Grund in Report.ErrorMessage

Die Verweigerungstore, die SaveAsOptionalContentConfigured in PDFium Component vor dem Schreiben einer konfigurierten Delphi-PDF durchsetzt: eine Objektnummer außerhalb der OCGs scheitert, gesperrte Gruppen scheitern, RBGroups-Sets mit mehr als einem sichtbaren Member werden zurückgewiesen, verschlüsselte Dokumente können keine Klartext-Inkremental-Objekte tragen, und signierte Dateien verlangen AllowSignedDocument
Jede Verweigerung passiert, bevor das Ziel angefasst wird; der Grund landet in Report.ErrorMessage, bei signierter Datei in der EPdfError-Meldung

Zwei dieser Tore gehen über das hinaus, was die Spezifikation streng verlangt. §8.11.4.3 beschreibt /Locked als Schutz vor Änderungen über die Benutzeroberfläche des Viewers und beschreibt Radio-Button-Gruppen als Abschalten der anderen Member, wenn einer eingeschaltet wird. PDFium Component wendet beides auch auf programmatische Änderungen an, und bei Radiogruppen verweigert es, statt Member zum Abschalten zu erwählen. Verlangt man zwei Member einer sich gegenseitig ausschließenden Menge, kann die Bibliothek nicht wissen, welchen Sie meinten, und eine Vermutung würde Ihrer Anfrage widersprechen. Schalten Sie die anderen im selben Changes-Array ab, und der Save geht durch

Das Signatur-Tor existiert, weil die angehängte Revision auf PDFiums vollem Re-Save sitzt, das eine neue Bytefolge ist; die Byte-Range, die eine bestehende Signatur abdeckt, überlebt das nicht notwendigerweise. Übergeben Sie AllowSignedDocument = True nur, wenn der Verlust dieser Signatur akzeptabel ist

Drei Seiteneffekte zählen in einem Batch-Job. Das Umschreiben normalisiert die gewählte Konfiguration auf /BaseState /ON, also wird die /Unchanged- oder /OFF-Baseline eines Autors zu expliziten Arrays mit derselben resultierenden Sichtbarkeit. Das Streichen von /AS entfernt auch Druck-Tricks wie einen Wasserzeichen-Layer, der nur auf Papier erscheint; setzen Sie DisableAutomaticState auf False, um diese Regeln zu behalten, und akzeptieren Sie, dass sie Ihren angefragten Zustand für ihr Event überstimmen können. Und die /ON- und /OFF-Arrays werden aus den für Options.Usage berechneten Zuständen geschrieben, sodass ein Druck-Preset in Konfiguration 0 auch ändert, was der Bildschirm für jede Gruppe ohne Usage-Wörterbuch zeigt. Halten Sie Druck-Presets in einem /Configs-Eintrag und überlassen Sie /D dem Bildschirm. Auf der Compliance-Seite verbieten PDF/A-2 (ISO 19005-2 clause 6.9) und PDF/UA (ISO 14289-1 clause 7.10) /AS in Konfigurationswörterbüchern, also räumt die Standardausgabe einen Befund weg, den Ihre PDF/A-Preflight-Validierung mit PDFium Component sonst melden würde

Layer-Änderungen innerhalb eines Delphi-Viewers anwenden

In einem Viewer ist Layersteuerung eine Checkliste, gebaut aus dem Inventar, gefolgt von einem Save in den Speicher und einem Reload. Die Checkliste aus Groups füllen, Einträge mit gesetztem Locked-Flag deaktivieren, Member mit gemeinsamem RadioGroupIndex als gegenseitig ausschließend behandeln und beim Anwenden über den Stream-Overload in einen TMemoryStream schreiben. TPdf.LoadDocument erwartet eine inaktive Komponente, also zuerst abschalten; das gepufferte Laden kopiert die Bytes, sodass Sie den Stream sofort freigeben können

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);       // signierte Datei: Output ist noch leer
        Exit;
      end;
    end;
    KeepPage := Pdf.PageNumber;
    Pdf.Active := False;
    Pdf.LoadDocument(Output);         // Buffered hat die Voreinstellung True
    Pdf.PageNumber := KeepPage;
  finally
    Output.Free;
  end;
end;

Nach dem Reload malt PDFium aus der neuen Konfiguration, sodass Bildschirm und nächster InspectOptionalContent-Aufruf übereinstimmen. Die Verdrahtung zwischen TPdf und TPdfView behandelt der Artikel zum Bau eines funktionsreichen PDF-Viewers mit PDFium VCL in Delphi. Hat ein Dokument noch keine Layer und Sie brauchen welche, führt PDF-Layer mit OCG und OCMD in Delphi anlegen durch SaveAsOptionalContentCreated

Preise, der Trial-Build und die übrigen Dokumentfeatures der Komponente stehen auf der Produktseite PDFium Component für Delphi