Technical Article

PDF-lag i Delphi: Valgfrie indholdsgrupper (OCG)

En landmåler åbner en situationsplan og ønsker højdekurverne skjult, mens forsyningsledningerne forbliver synlige. En anmelder ønsker korrekturannoteringer (redlines) synlige på skærmen, men væk fra udskriften. Et produktark leveres på tre sprog i én fil, og læseren vælger, hvilket sprog der skal vises. Alle tre er den samme PDF-funktion, og panelet, der styrer dem i Acrobat, kaldes Lag. Funktionen bag dette panel er valgfrit indhold (optional content), og det er det, der gør det muligt for en enkelt side at bære flere uafhængige visuelle lag, som en fremviser tænder og slukker for.

Valgfrit indhold er specificeret i ISO 32000-1 §8.11. Synlighedsenheden er en valgfri indholdsgruppe, en OCG, en ordbog af typen /OCG, der bærer et navn. Markeret indhold på en side er knyttet til en gruppe, og fremviseren bestemmer, om den pågældende gruppe aktuelt vises. En beslægtet konstruktion, optional content membership dictionary eller OCMD, lader synligheden afhænge af en boolesk kombination af flere grupper, men det almindelige tilfælde er en enkelt navngiven gruppe, der repræsenterer et enkelt lag. Dokumentet binder hele mekanismen sammen via en enkelt katalogpost, /OCProperties, som beskrives i det følgende.

Hvad kataloget skal indeholde

En OCG er i sig selv inaktiv. For at en fremviser kan liste et lag og huske dets tilstand, har dokumentkataloget brug for en /OCProperties-ordbog, og §8.11.4 fastlægger præcis, hvad der skal være i den. Der er et /OCGs-array, der navngiver alle grupper i filen, og der er en /D-post, der indeholder standardkonfigurationen. Standardkonfigurationen er den del, en læser anvender, når filen åbnes første gang. Den registrerer, hvilke grupper der starter som tændt, og hvilke der starter som slukket, hvilke poster der er låst mod at brugeren ændrer dem, og – via et /Order-array – hvordan lagnavnene er arrangeret og indlejret i panelet.

Den praktiske konsekvens er, at oprettelse af et lag never er en ren lokal handling. Gruppen skal tegnes på siden, og den skal også registreres i en struktur på katalogniveau, som ikke eksisterede før. PDFlibPas gør begge dele for dig. Det første kald, der opretter en gruppe, tilføjer /OCProperties-posten til kataloget og sår standardkonfigurationen, så laget både tegnes og listes uden separat bogføring fra din side.

Hvorfor en overholdelsestilstand kan tilbageholde funktionen

Før nogen lagkode køres, bestemmer dokumentets overholdelsesmål, om valgfrit indhold overhovedet er tilladt. PDF/A-1, arkiveringsprofilen defineret i ISO 19005-1, forbyder /OCProperties-posten direkte i §6.1.13. Ræsonnementet passer til formatets formål. En arkivfil skal renderes identisk for enhver læser langt ind i fremtiden, og indhold, hvis synlighed en fremviser kan ændre, er indhold, hvis udseende ikke er fastlåst. Derfor forbyder profilen konstruktionen i stedet for at tillade et tvetydigt arkiv. PDF/A-2 og PDF/A-3, defineret i ISO 19005-2 and ISO 19005-3, indtager det modsatte synspunkt i deres §6.9 og tillader valgfrit indhold, med regler om standardsynlighed.

Den forskel viser sig direkte i API'en. Når dokumentet er i en PDF/A-1-tilstand, nægter NewOptionalContentGroup at oprette gruppen og returnerer nul, fordi opfyldelse af anmodningen ville producere en fil, der fejler i forhold til sin egen erklærede overholdelse. I PDF/A-2- eller PDF/A-3-tilstand samt i almindelig ubegrænset PDF lykkes det samme kald og returnerer et gruppe-ID, der er forskelligt fra nul. Et nul-resultat er derfor ikke en generel fejl, man skal undersøge senere; det er biblioteket, der fortæller dig, at det aktive overholdelsesniveau ikke har plads til funktionen.

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
      // refused under PDF/A-1; not a transient error, the mode bans layers
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

To tilstande pr. lag, ikke én

Et lag er ikke blot synligt eller usynligt. Standardkonfigurationen registrerer dets tilstand på skærmen og en separat printtilstand, fordi §8.11.4 skelner mellem det, en fremviser viser, og det, en udskrivningspipeline udsender. De to er uafhængige med vilje. Et udkast-vandmærke kan vises på skærmen og udelades på papir, og et skærelinje-lag kan skjules på skærmen, men sendes til a plotter. At slå de to sammen ville tvinge det ene til at følge det andet og miste præcis den kontrol, som funktionen eksisterer for at give.

PDFlibPas eksponerer parret via to settere. SetOptionalContentGroupVisible tager gruppe-ID'et og et flag, hvor et-tal betyder synligt og nul betyder skjult, og styrer standardtilstanden på skærmen. SetOptionalContentGroupPrintable tager gruppe-ID'et og et flag for, om laget skal udsendes, når dokumentet udskrives. De matchende gettere, GetOptionalContentGroupVisible og GetOptionalContentGroupPrintable, returnerer hver især et eller nul, så du kan aflæse et lags skærm- og printstatus separat i stedet for at udlede den ene af den anden.

Opbygning af to lag på en side

Oprettelse af et lag og udfyldning af det følger en fast rækkefølge. Du tegner indholdet til laget på den aktuelle side og kalder derefter SetContentStreamOptional med gruppe-ID'et, hvilket indkapsler sidens aktuelle indholdsstrøm, så alt tegnet indtil nu tilhører den pågældende gruppe. Fordi kaldet fanger alt, hvad der er på strømmen i det øjeblik, er disciplinen at lægge et lags markeringer, tildele dem og først derefter starte det næste lag. Eksemplet nedenfor placerer forsyningsledninger på den første side og en anmelder-redline på en anden side, indstiller hvert lags skærm- og printtilstand og gemmer.

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: utilities, drawn then assigned to its own group
    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);   // shown on screen
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper

    // Layer 2: reviewer redline on a fresh page
    Pdf.InsertPages(2, 1);                     // append one page after page 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);    // visible while reviewing
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // never printed

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

Korrekturlaget (redline) er værd at bemærke. Det vises på skærmen, så en anmelder ser noten, og dets printbare flag er nul, så en udskrift af den samme fil ikke indeholder korrekturskriften. Den asymmetri er hele pointen med at holde de to tilstande adskilt.

Aflæsning af konfigurationen

Aflæsning af lag er en anden gennemgang af den samme struktur. Efter at en fil er indlæst, rapporterer GetOptionalContentConfigCount, hvor mange konfigurationsordbøger dokumentet indeholder; den første standardkonfiguration er konfigurations-ID 1. Inden for en konfiguration giver GetOptionalContentConfigOrderCount antallet af poster i rækkefølgetræet (order tree), og du indekserer dem fra 1. For hver post returnerer GetOptionalContentConfigOrderItemLabel dens visningstekst, og GetOptionalContentConfigOrderItemLevel returnerer dens indlejringsdybde, så en paneloversigt med underlag indrykket under overskrifter kan rekonstrueres ordret.

Hver post har også en type. GetOptionalContentConfigOrderItemType skelner en faktisk valgfri indholdsgruppe fra en ren tekstetiket, der kun eksisterer for at stå som overskrift for en sektion i træet. Den skelnen er vigtig, fordi tilstandsforespørgsler pr. gruppe kun giver mening for rigtige grupper. For en gruppepost rapporterer GetOptionalContentConfigState, om konfigurationen starter den som tændt, slukket eller efterlader den uændret, og GetOptionalContentConfigLocked rapporterer, om brugeren er forhindret i at ændre den. Løkken nedenfor renderer rækkefølgetræet med hver gruppes tilstand og låsestatus indrykket efter 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;                                  // the default configuration
    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 is a text label heading; it has no per-group state

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

To detaljer holder denne løkke korrekt. Rækkefølgeindekset er et-baseret, fra 1 til antallet, hvilket svarer til, hvordan biblioteket nummererer træet internt. Og kaldene pr. gruppe kører kun, når elementtypen er en gruppe, fordi en tekstetiket er en overskrift med et navn og et niveau, men uden en tændt-, slukket- eller låst-tilstand at forespørge på. Springer du den beskyttelse over, beder du en etiket om en tilstand, den ikke har.

Hvor dette passer ind

Lag er en præsentationsmekanisme, så motoren skal respektere dem på enhver sti, der renderer en side, og renderesiden er dækket i vores gennemgang af multi-motor-rendering i Delphi. De krydser også dokumentets struktur, fordi et lags navn er tekst rettet mod forfatteren, og en læser har gavn af en struktureret lag oversigt, hvilket forbinder til arbejdet i vores artikel om tagget PDF og tilgængelighedsstruktur. Begge parres med de valgfri indholds-API'er, der beskrives her, som leveres som en del af Delphi PDF Library sammen med funktionerne til sider, tekst, skrifttyper og overholdelse, der diskuteres andre steder på denne blog.