Technischer Artikel

PDF-Ebenen in Delphi: Optional Content Groups (OCG)

Ein Vermessungsingenieur öffnet einen Lageplan und möchte die Höhenlinien ausblenden, während die Versorgungsleitungen sichtbar bleiben. Ein Prüfer möchte die Redline-Anmerkungen am Bildschirm sehen, aber nicht im Ausdruck. Ein Produktdatenblatt wird in drei Sprachen aus einer einzigen Datei ausgeliefert, und der Leser wählt, welche Sprache angezeigt wird. Alle drei Fälle beruhen auf derselben PDF-Funktion, und das Panel, das sie in Acrobat steuert, heißt Ebenen. Die Funktion hinter diesem Panel ist optionaler Inhalt, und sie erlaubt es, dass eine einzelne Seite mehrere unabhängige visuelle Schichten trägt, die ein Viewer ein- und ausschaltet

Optionaler Inhalt ist in ISO 32000-1 §8.11 spezifiziert. Die Einheit der Sichtbarkeit ist eine Optional Content Group, eine OCG, ein Dictionary vom Typ /OCG, das einen Namen trägt. Markierter Inhalt auf einer Seite wird einer Gruppe zugeordnet, und der Viewer entscheidet, ob diese Gruppe gerade angezeigt wird. Ein verwandtes Konstrukt, das Optional Content Membership Dictionary oder OCMD, lässt die Sichtbarkeit von einer booleschen Kombination mehrerer Gruppen abhängen, doch der Alltagsfall ist eine einzelne benannte Gruppe, die für eine einzelne Ebene steht. Das Dokument verknüpft den gesamten Mechanismus über einen einzigen Katalogeintrag, /OCProperties, der als Nächstes beschrieben wird

Was der Katalog enthalten muss

Eine OCG für sich allein ist wirkungslos. Damit ein Viewer eine Ebene auflistet und ihren Zustand behält, braucht der Dokumentkatalog ein /OCProperties-Dictionary, und §8.11.4 legt genau fest, was hineingehört. Es gibt ein /OCGs-Array, das jede Gruppe in der Datei benennt, und einen /D-Eintrag, der die Standardkonfiguration hält. Die Standardkonfiguration ist der Teil, den ein Reader beim ersten Öffnen der Datei anwendet. Sie hält fest, welche Gruppen eingeschaltet und welche ausgeschaltet starten, welche Einträge gegen das Umschalten durch den Benutzer gesperrt sind und, über ein /Order-Array, wie die Ebenennamen im Panel angeordnet und verschachtelt sind

Struktur der optionalen Inhaltsgruppen in PDF hinter PDF Library for Delphi: Der Katalogeintrag OCProperties listet jede OCG-Ebene und ihre Standardkonfiguration mit Startzuständen, Sperren und Panel-Reihenfolge, während markierter Seiteninhalt Zeichnungen an Gruppen bindet
Ebenen werden erst über den Katalog adressierbar: OCProperties listet jede OCG, und die Standardkonfiguration hält Startzustände, Sperren und Panel-Reihenfolge fest

Die praktische Konsequenz ist, dass das Anlegen einer Ebene nie ein rein lokaler Akt ist. In die Gruppe muss auf der Seite gezeichnet werden, und sie muss zugleich in einer Struktur auf Katalogebene registriert werden, die vorher nicht existierte. PDF Library for Delphi erledigt beides für Sie. Der erste Aufruf, der eine Gruppe erzeugt, fügt dem Katalog den /OCProperties-Eintrag hinzu und legt die Standardkonfiguration an, sodass die Ebene sowohl gezeichnet als auch aufgelistet wird, ohne dass Sie separat Buch führen müssen

Warum ein Konformitätsmodus die Funktion verweigern kann

Bevor irgendein Ebenen-Code läuft, entscheidet das Konformitätsziel des Dokuments, ob optionaler Inhalt überhaupt zulässig ist. PDF/A-1, das in ISO 19005-1 definierte Archivprofil, verbietet den /OCProperties-Eintrag in §6.1.13 rundheraus. Die Begründung passt zum Zweck des Formats. Eine Archivdatei muss für jeden Reader bis weit in die Zukunft identisch gerendert werden, und Inhalt, dessen Sichtbarkeit ein Viewer ändern kann, ist Inhalt, dessen Erscheinungsbild nicht festgelegt ist; das Profil verbietet das Konstrukt daher lieber, als ein mehrdeutiges Archiv zuzulassen. PDF/A-2 und PDF/A-3, definiert in ISO 19005-2 und ISO 19005-3, vertreten in ihrem §6.9 die Gegenposition und erlauben optionalen Inhalt, mit Regeln zur Standardsichtbarkeit

Dieser Unterschied schlägt direkt auf die API durch. Befindet sich das Dokument in einem PDF/A-1-Modus, verweigert NewOptionalContentGroup das Anlegen der Gruppe und gibt null zurück, weil das Erfüllen der Anfrage eine Datei erzeugen würde, die ihre eigene deklarierte Konformität verletzt. Im PDF/A-2- oder PDF/A-3-Modus sowie in gewöhnlichem, uneingeschränktem PDF gelingt derselbe Aufruf und liefert eine Gruppen-ID ungleich null. Ein Ergebnis von null ist daher kein generischer Fehler, den Sie später untersuchen; es ist die Bibliothek, die Ihnen mitteilt, dass die aktive Konformitätsstufe keinen Platz für die Funktion hat

var
  Pdf: TPDFlib;
  LayerID: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);                       // PDF/A-1a: OCProperties verboten

    LayerID := Pdf.NewOptionalContentGroup('Utilities');
    if LayerID = 0 then
      // unter PDF/A-1 verweigert; kein vorübergehender Fehler, der Modus verbietet Ebenen
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

Zwei Zustände pro Ebene, nicht einer

Eine Ebene ist nicht einfach sichtbar oder unsichtbar. Die Standardkonfiguration hält ihren Bildschirmzustand und einen getrennten Druckzustand fest, weil §8.11.4 unterscheidet, was ein Viewer anzeigt, von dem, was eine Druck-Pipeline ausgibt. Beide sind absichtlich unabhängig. Ein Entwurfs-Wasserzeichen kann am Bildschirm erscheinen und auf Papier entfallen, und eine Schnittlinien-Ebene kann am Bildschirm verborgen sein und dennoch an einen Plotter gehen. Beide zusammenzulegen würde eines dem anderen unterordnen und genau die Kontrolle verlieren, für die die Funktion da ist

PDF Library for Delphi stellt das Paar über zwei Setter bereit. SetOptionalContentGroupVisible nimmt die Gruppen-ID und ein Flag entgegen, wobei eins sichtbar und null verborgen bedeutet, und steuert den Standard-Bildschirmzustand. SetOptionalContentGroupPrintable nimmt die Gruppen-ID und ein Flag dafür entgegen, ob die Ebene beim Drucken des Dokuments ausgegeben wird. Die passenden Getter, GetOptionalContentGroupVisible und GetOptionalContentGroupPrintable, geben jeweils eins oder null zurück, sodass Sie Bildschirm- und Druckverhalten einer Ebene getrennt zurücklesen können, statt eines aus dem anderen abzuleiten

Matrix unabhängiger Bildschirm- und Druckzustände für PDF-Ebenen, die mit der Delphi PDF Library erstellt wurden: Versorgungsebenen bleiben überall sichtbar, Prüfer-Markup erscheint am Bildschirm, wird aber nie gedruckt, und eine Plotter-Schnittlinie ist am Bildschirm verborgen und wird dennoch gedruckt
Bildschirmsichtbarkeit und Druckausgabe werden pro Gruppe gesetzt, sodass Versorgungsebenen überall erscheinen können, während Prüfer-Markup eine reine Bildschirmnotiz bleibt

Zwei Ebenen auf einer Seite aufbauen

Das Anlegen und Befüllen einer Ebene folgt einer festen Reihenfolge. Sie zeichnen den Inhalt der Ebene auf die aktuelle Seite und rufen dann SetContentStreamOptional mit der Gruppen-ID auf, was den aktuellen Content-Stream der Seite umschließt, sodass alles bisher Gezeichnete zu dieser Gruppe gehört. Da der Aufruf erfasst, was in diesem Moment im Stream liegt, lautet die Disziplin: die Zeichenbefehle einer Ebene ablegen, sie zuweisen und erst dann mit der nächsten Ebene beginnen. Das folgende Beispiel legt Versorgungsleitungen auf die erste Seite und eine Prüfer-Redline auf eine zweite Seite, setzt für jede Ebene Bildschirm- und Druckzustand und speichert

var
  Pdf: TPDFlib;
  FontID, UtilLayer, RedlineLayer: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;                          // uneingeschränktes PDF: Ebenen erlaubt
    Pdf.SetPageDimensions(595, 842);          // A4 in Punkt
    FontID := Pdf.AddStandardFont(0);         // Helvetica
    Pdf.SelectFont(FontID);

    // Ebene 1: Versorgungsleitungen, gezeichnet und dann der eigenen Gruppe zugewiesen
    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);   // am Bildschirm angezeigt
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // und auf Papier

    // Ebene 2: Prüfer-Redline auf einer neuen Seite
    Pdf.InsertPages(2, 1);                     // eine Seite nach Seite 1 anhängen
    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);    // sichtbar während der Prüfung
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // nie gedruckt

    Pdf.SaveToFile('SitePlan_Layers.pdf');
  finally
    Pdf.Free;
  end;
end;

Die Redline-Ebene ist der Fall, der Beachtung verdient. Sie wird am Bildschirm angezeigt, damit ein Prüfer die Notiz sieht, und ihr Printable-Flag ist null, sodass ein Ausdruck derselben Datei keinen Prüftext enthält. Diese Asymmetrie ist der ganze Sinn davon, die beiden Zustände getrennt zu halten

Die Konfiguration zurücklesen

Ebenen zu lesen ist ein anderer Gang durch dieselbe Struktur. Nach dem Laden einer Datei meldet GetOptionalContentConfigCount, wie viele Konfigurations-Dictionarys das Dokument enthält; die erste Standardkonfiguration hat die Konfigurations-ID 1. Innerhalb einer Konfiguration liefert GetOptionalContentConfigOrderCount die Zahl der Einträge im Order-Baum, und Sie indizieren sie ab 1. Für jeden Eintrag gibt GetOptionalContentConfigOrderItemLabel den Anzeigetext und GetOptionalContentConfigOrderItemLevel die Verschachtelungstiefe zurück, sodass eine Panel-Gliederung mit unter Überschriften eingerückten Unterebenen wortgetreu rekonstruiert werden kann

Jeder Eintrag hat außerdem einen Typ. GetOptionalContentConfigOrderItemType unterscheidet eine echte Optional Content Group von einer reinen Textbeschriftung, die nur als Überschrift eines Baumabschnitts existiert. Diese Unterscheidung ist wichtig, weil die Zustandsabfragen pro Gruppe nur für echte Gruppen sinnvoll sind. Für einen Gruppeneintrag meldet GetOptionalContentConfigState, ob die Konfiguration ihn eingeschaltet, ausgeschaltet oder unverändert startet, und GetOptionalContentConfigLocked meldet, ob der Benutzer am Umschalten gehindert wird. Die folgende Schleife gibt den Order-Baum mit Zustand und Sperrstatus jeder Gruppe aus, nach Ebene eingerückt

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;                                  // die Standardkonfiguration
    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 ist eine Textbeschriftung; sie hat keinen Gruppenzustand

      Writeln(Line);
    end;
  finally
    Pdf.Free;
  end;
end;

Zwei Details halten diese Schleife korrekt. Der Order-Index ist einsbasiert, von 1 bis zur Anzahl, passend dazu, wie die Bibliothek den Baum intern nummeriert. Und die Aufrufe pro Gruppe laufen nur, wenn der Eintragstyp eine Gruppe ist, weil eine Textbeschriftung eine Überschrift mit Name und Ebene ist, aber keinen Ein-, Aus- oder Sperrzustand zum Abfragen hat. Lassen Sie diese Prüfung weg, fragen Sie eine Beschriftung nach einem Zustand, den sie nicht besitzt

Rücklese-Schleife für optionale Inhaltskonfigurationen in PDF Library for Delphi: Konfigurationen aufzählen, Order-Einträge einsbasiert durchlaufen und dann nach ItemType verzweigen, sodass nur echte Gruppen nach Ein-, Aus- oder Sperrzustand gefragt werden, während Textbeschriftungen übersprungen werden
Das Zurücklesen der Ebenen durchläuft die Order-Einträge einsbasiert und fragt den Zustand nur für echte Gruppen ab, weil eine Beschriftungsüberschrift keinen Ein-, Aus- oder Sperrzustand zu melden hat

Wo das hineinpasst

Ebenen sind ein Darstellungsmechanismus, daher muss die Engine sie auf jedem Pfad berücksichtigen, der eine Seite rendert; die Rendering-Seite behandelt unser Leitfaden zum Multi-Engine-Rendering in Delphi. Sie überschneiden sich auch mit der Dokumentstruktur, weil der Name einer Ebene autorenseitiger Text ist und ein Reader von einer strukturierten Ebenengliederung profitiert, was an die Arbeit in unserem Artikel zu Tagged PDF und Barrierefreiheitsstruktur anknüpft. Beides ergänzt die hier beschriebenen APIs für optionalen Inhalt, die als Teil der Delphi PDF Library zusammen mit den an anderer Stelle in diesem Blog behandelten Funktionen für Seiten, Text, Schriften und Konformität ausgeliefert werden