Technisch artikel

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

Een landmeter opent een situatieplan en wil de contouren verbergen terwijl de nutsvoorzieningen zichtbaar blijven. Een beoordelaar wil de rode annotaties op het scherm zien, maar niet op de afdruk. Een productblad wordt in drie talen vanuit één bestand verzonden, en de lezer kiest welke taal wordt weergegeven. Alle drie zijn ze gebaseerd op dezelfde PDF-functie, en het paneel dat ze in Acrobat aanstuurt heet Lagen (Layers). De onderliggende functie van dat paneel is optionele inhoud (optional content), en hiermee kan een enkele pagina verschillende onafhankelijke visuele lagen bevatten die een gebruiker kan in- en uitschakelen

Optionele inhoud is gespecificeerd in ISO 32000-1 §8.11. De eenheid van zichtbaarheid is een optionele inhoudsgroep (optional content group), een OCG, een dictionary van het type /OCG die een naam draagt. Gemarkeerde inhoud op een pagina is gekoppeld aan een groep, en de viewer bepaalt of die groep momenteel wordt getoond. Een verwante structuur, de optionele inhouds-lidmaatschapsdictionary (optional content membership dictionary) of OCMD, maakt de zichtbaarheid afhankelijk van een boolean combinatie van verschillende groepen, maar in het dagelijks gebruik staat een enkele benoemde groep voor een enkele laag. Het document verbindt het hele mechanisme via één catalogusingang, /OCProperties, die hierna wordt beschreven

Wat de catalogus moet bevatten

Een OCG op zichzelf is inactief. Om ervoor te zorgen dat een viewer een laag kan weergeven en de status ervan kan onthouden, heeft de documentcatalogus een /OCProperties-dictionary nodig, en §8.11.4 legt precies uit wat daarin hoort. Er is een /OCGs-array die elke groep in het bestand benoemt, en er is een /D-ingang die de standaardconfiguratie bevat. De standaardconfiguratie is het deel dat een lezer toepast wanneer het bestand voor het eerst wordt geopend. Het legt vast welke groepen ingeschakeld starten en welke uitgeschakeld starten, welke vermeldingen zijn vergrendeld zodat de gebruiker ze niet kan omschakelen, en, via een /Order-array, hoe de laagnamen zijn gerangschikt en genest in het paneel

Structuur van PDF optional content groups achter PDF Library for Delphi: de catalog-OCProperties-entry somt elke OCG-laag op met zijn standaardconfiguratie, starttoestanden, vergrendelingen en paneelvolgorde, terwijl page marked content tekeningen aan groepen bindt
Lagen worden alleen adresseerbaar via de catalog: OCProperties somt elke OCG op en de standaardconfiguratie legt startstatussen, vergrendelingen en paneelvolgorde vast

Het praktische gevolg is dat het maken van een laag nooit een puur lokale handeling is. De groep moet op de pagina worden getekend, en moet ook worden geregistreerd in een structuur op catalogusniveau die voorheen niet bestond. PDF Library for Delphi doet beide voor u. De eerste aanroep die een groep maakt, voegt de vermelding /OCProperties toe aan de catalogus en initialiseert de standaardconfiguratie, zodat de laag zowel getekend als vermeld wordt zonder dat u zelf extra administratie hoeft bij te houden

Waarom een nalevingsmodus de functie kan weigeren

Voordat er laagcode wordt uitgevoerd, bepaalt het conformiteitsdoel van het document of optionele inhoud überhaupt is toegestaan. PDF/A-1, het archiefprofiel gedefinieerd in ISO 19005-1, verbiedt de /OCProperties-vermelding volledig in §6.1.13. De redenering past bij het doel van het formaat. Een archiefbestand moet tot ver in de toekomst voor elke lezer identiek worden weergegeven, en inhoud waarvan de zichtbaarheid door een viewer kan worden gewijzigd, is inhoud waarvan het uiterlijk niet vaststaat. Het profiel verbiedt daarom deze structuur om een dubbelzinnig archief te voorkomen. PDF/A-2 en PDF/A-3, gedefinieerd in ISO 19005-2 en ISO 19005-3, nemen het tegenovergestelde standpunt in in hun §6.9 en staan optionele inhoud toe, met regels over de standaardzichtbaarheid

Dat verschil is direct terug te zien in de API. Wanneer het document is in een PDF/A-1-modus, NewOptionalContentGroup weigert de groep aan te maken en retourneert nul, omdat het inwilligen van het verzoek een bestand zou opleveren dat niet voldoet aan de eigen gedeclareerde conformiteit. In de PDF/A-2- of PDF/A-3-modus, en in gewone onbeperkte PDF, slaagt dezelfde aanroep en retourneert een groeps-ID die ongelijk is aan nul. Een nulresultaat is daarom geen algemene fout om later te inspecteren; het is de bibliotheek die u vertelt dat het actieve nalevingsniveau geen ruimte heeft voor deze functie

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

    LayerID := Pdf.NewOptionalContentGroup('Utilities');
    if LayerID = 0 then
      // geweigerd onder PDF/A-1; geen tijdelijke fout, deze modus verbiedt layers
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

Twee statussen per laag, niet één

Een laag is niet simpelweg zichtbaar of onzichtbaar. De standaardconfiguratie registreert de status op het scherm en een afzonderlijke afdrukstatus, omdat §8.11.4 onderscheid maakt tussen wat een viewer weergeeft en wat een afdruktraject produceert. De twee zijn bewust onafhankelijk van elkaar. Een conceptwatermerk kan op het scherm worden getoond maar bij het afdrukken worden weggelaten, en een snijlijnlaag kan op het scherm worden verborgen maar wel naar een plotter worden gestuurd. Het samenvoegen van de twee zou ertoe leiden dat de ene de andere moet volgen, waardoor u precies de controle verliest waarvoor de functie is ontworpen

PDF Library for Delphi stelt het paar beschikbaar via twee setters. SetOptionalContentGroupVisible vereist het groeps-ID en een vlag (waarbij één zichtbaar betekent en nul verborgen) en regelt de standaardstatus op het scherm. SetOptionalContentGroupPrintable vereist het groeps-ID en een vlag die bepaalt of de laag wordt uitgevoerd wanneer het document wordt afgedrukt. De bijbehorende getters, GetOptionalContentGroupVisible en GetOptionalContentGroupPrintable, retourneren elk één of nul, zodat u de scherm- en afdrukstatus van een laag afzonderlijk kunt uitlezen in plaats van de ene uit de andere af te leiden

Matrix van onafhankelijke scherm- en afdrukzichtbaarheidstoestanden voor PDF-lagen gemaakt met de Delphi PDF Library: hulplagen blijven overal zichtbaar, reviewer-markering verschijnt op het scherm maar wordt nooit afgedrukt, en een plotter-snijlijn verbergt zich op het scherm maar wordt toch afgedrukt
Schermzichtbaarheid en print-uitzending worden per groep ingesteld, zodat hulplagen overal kunnen verschijnen terwijl reviewer-markup een alleen-op-het-scherm-aantekening blijft

Twee lagen bouwen op een pagina

Het maken en vullen van een laag volgt een vaste volgorde. U tekent de inhoud voor de laag op de huidige pagina en roept vervolgens SetContentStreamOptional aan met het groeps-ID. Dit verpakt de huidige inhoudsstroom van de pagina, zodat alles wat tot nu toe is getekend bij die groep hoort. Omdat de aanroep alles vastlegt wat zich op dat moment in de stroom bevindt, is het de kunst om de markeringen van de ene laag aan te brengen, deze toe te wijzen en pas daarna met de volgende laag te beginnen. Het onderstaande voorbeeld plaatst nutsvoorzieningen op de eerste pagina en de revisiemarkeringen op een tweede pagina, stelt de scherm- en afdrukstatus van elke laag in en slaat het document op

var
  Pdf: TPDFlib;
  FontID, UtilLayer, RedlineLayer: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;                          // unconstrained PDF: layers allowed
    Pdf.SetPageDimensions(595, 842);          // A4 in points
    FontID := Pdf.AddStandardFont(0);         // Helvetica
    Pdf.SelectFont(FontID);

    // Layer 1: nutsleidingen, eerst getekend en daarna aan een eigen group toegewezen
    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);   // zichtbaar op het scherm
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // en op papier

    // Layer 2: redline van de reviewer op een nieuwe pagina
    Pdf.InsertPages(2, 1);                     // voeg één pagina toe na pagina 1
    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);    // zichtbaar tijdens het reviewen
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // never printed

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

De revisielaag (redline layer) is hierbij het vermelden waard. Deze wordt op het scherm getoond zodat een beoordelaar de opmerking ziet, maar de afdrukbare vlag (printable flag) is ingesteld op nul, waardoor een afdruk van hetzelfde bestand geen revisietekst bevat. Die asymmetrie is precies de reden om de twee statussen gescheiden te houden

De configuratie teruglezen

Het lezen van lagen is een andere tocht door dezelfde structuur. Nadat een bestand is geladen, GetOptionalContentConfigCount rapporteert hoeveel configuratiedictionaries het document bevat; de eerste standaardconfiguratie is configuratie-ID 1. Binnen een configuratie geeft GetOptionalContentConfigOrderCount het aantal vermeldingen in de volgordeboom (order tree) aan, en u indexeert deze vanaf 1. Voor elke vermelding retourneert GetOptionalContentConfigOrderItemLabel de weergavetekst en GetOptionalContentConfigOrderItemLevel het nestingsniveau, zodat een paneelstructuur met sublagen die onder koppen zijn ingesprongen, letterlijk kan worden gereconstrueerd

Elke vermelding heeft ook een type. GetOptionalContentConfigOrderItemType maakt onderscheid tussen een daadwerkelijke optionele inhoudsgroep en een gewoon tekstlabel dat alleen dient als kop van een sectie in de boom. Dat onderscheid is belangrijk omdat statusopvragingen per groep alleen zinvol zijn voor echte groepen. Voor een groepsvermelding rapporteert GetOptionalContentConfigState of de configuratie deze inschakelt (on), uitschakelt (off) of ongewijzigd laat, en GetOptionalContentConfigLocked rapporteert of het de gebruiker is verboden deze om te schakelen. De onderstaande lus geeft de volgordeboom weer met de status en vergrendelingsstatus van elke groep, ingesprongen per niveau

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;                                  // de standaardconfiguratie
    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 = een 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 is een tekstlabel-kop; die heeft geen state per group

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

Twee details zorgen ervoor dat deze lus correct blijft. De volgorde-index is 1-gebaseerd, van 1 tot het totale aantal, wat overeenkomt met hoe de bibliotheek de boom intern nummert. En de aanroepen per groep worden alleen uitgevoerd als het itemtype een groep is, omdat een tekstlabel een kop is met een naam en een niveau, maar zonder een aan-, uit- of vergrendelde status om op te vragen. Sla die beveiliging over en u vraagt een label naar een status die het niet heeft

Terugsleeslus voor optional content-configuraties in PDF Library for Delphi: configuraties opsommen, order-entries aflopen vanaf één, dan vertakken op ItemType zodat alleen echte groepen worden bevraagd op on-, off- of locked-toestand terwijl tekstlabels worden overgeslagen
Het teruglezen van lagen doorloopt de order-items één-gebaseerd en raadpleegt state alleen voor echte groepen, want een labelkop heeft geen on-, off- of locked-status te rapporteren

Waar dit in het grotere geheel past

Lagen zijn een presentatiemechanisme, dus de engine moet ze respecteren in elk traject dat een pagina rendert. De renderingzijde wordt behandeld in onze handleiding over multi-engine rendering in Delphi. Ze kruisen ook met de documentstructuur, omdat de naam van een laag tekst is die voor de auteur bestemd is en een lezer baat heeft bij een gestructureerd laagoverzicht, wat aansluit bij het werk in ons artikel over tagged PDF en toegankelijkheidsstructuur. Beide sluiten aan bij de API's voor optionele inhoud die hier worden beschreven en die worden geleverd als onderdeel van de Delphi PDF Library, naast de functionaliteiten voor pagina's, tekst, lettertypen en naleving die elders op deze blog worden besproken