Technical Article

PDF-lag i Delphi: Valgfrie innholdsgrupper (OCG)

En landmåler åpner en situasjonsplan og vil ha konturene skjult mens verktøyene forblir synlige. En evaluator vil ha rødlinjeannoteringene synlige på skjermen, men fjernet fra utskriften. Et produktark leveres på tre språk fra én fil, og leseren velger hvilket språk som skal vises. Alle tre er den samme PDF-funksjonen, og panelet som driver dem i Acrobat kalles Layers (Lag). Funksjonen under dette panelet er valgfritt innhold (optional content), og det er det som lar en enkelt side bære flere uavhengige visuelle lag som et visningsprogram slår på og av.

Valgfritt innhold er spesifisert i ISO 32000-1 §8.11. Enheten for synlighet er en valgfri innholdsgruppe, en OCG, en ordbok av typen /OCG som bærer et navn. Markert innhold på en side er knyttet til en gruppe, og visningsprogrammet bestemmer om denne gruppen vises for øyeblikket. En relatert konstruksjon, optional content membership dictionary eller OCMD, lar synligheten avhenge av en boolsk kombinasjon av flere grupper, men det daglige tilfellet er en enkelt navngitt gruppe som representerer et enkelt lag. Dokumentet binder hele mekanismen sammen gjennom én katalogoppføring, /OCProperties, som beskrives under.

Hva katalogen må inneholde

En OCG i seg selv er inaktiv. For at et visningsprogram skal liste et lag og huske tilstanden dets, trenger dokumentkatalogen en /OCProperties-ordbok, og §8.11.4 beskriver nøyaktig hva som går inn i den. Det er et /OCGs-array som navngir hver gruppe i filen, og det er en /D-oppføring som inneholder standardkonfigurasjonen. Standardkonfigurasjonen er den delen en leser bruker når filen åpnes for første gang. Den registrerer hvilke grupper som starter på og hvilke som starter av, hvilke oppføringer som er låst mot at brukeren endrer dem, og, via et /Order-array, hvordan lagnavnene er arrangert og nøstet i panelet.

Den praktiske konsekvensen er at det å opprette et lag aldri er en rent lokal handling. Gruppen må tegnes inn på siden, og den må også registreres i en struktur på katalognivå som ikke eksisterte fra før. PDFlibPas gjør begge deler for deg. Det første kallet som oppretter en gruppe, legger til /OCProperties-oppføringen i katalogen og setter inn standardkonfigurasjonen, slik at laget både blir tegnet og listet uten separat bokføring på din side.

Hvorfor en samsvarsmodus kan holde tilbake funksjonen

Før noen lagkode kjører, bestemmer dokumentets samsvarsmål om valgfritt innhold i det hele tatt er tillatt. PDF/A-1, arkiveringsprofilen definert i ISO 19005-1, forbyr /OCProperties-oppføringen fullstendig i §6.1.13. Begrunnelsen passer til formatets formål. En arkivfil må gjengis identisk for alle lesere langt inn i fremtiden, og innhold der synligheten kan endres av brukeren er innhold hvis utseende ikke er fastsatt, så profilen forbyr konstruksjonen i stedet for å tillate et tvetydig arkiv. PDF/A-2 og PDF/A-3, definert i ISO 19005-2 og ISO 19005-3, take motsatt syn i §6.9 og tillater valgfritt innhold, med regler for standardsynlighet.

Denne forskjellen viser seg direkte i API-et. Når dokumentet er i en PDF/A-1-modus, nekter NewOptionalContentGroup å opprette gruppen og returnerer null, fordi å etterkomme forespørselen ville produsere en fil som feiler i sitt eget erklærte samsvar. I PDF/A-2- eller PDF/A-3-modus, og i vanlig ubegrenset PDF, lykkes det samme kallet og returnerer en gruppe-ID som ikke er null. Et nullresultat er derfor ikke en generell feil du må inspisere senere; det er biblioteket som forteller deg at det aktive samsvarsnivået ikke har plass til funksjonen.

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 tilstander per lag, ikke én

Et lag er ikke bare synlig eller usynlig. Standardkonfigurasjonen registrerer dets tilstand på skjermen og en separat utskriftstilstand, fordi §8.11.4 skiller mellom hva et visningsprogram viser og hva en utskriftsprosess sender ut. De to er uavhengige med vilje. Et utkast-vannmerke kan vises på skjermen og utelates fra papir, og et kuttlinje-lag kan skjules på skjermen, men sendes til en plotter. Å slå sammen de to ville tvinge det ene til å følge det andre og miste akkurat den kontrollen funksjonen eksisterer for å gi.

PDFlibPas eksponerer paret gjennom to settere. SetOptionalContentGroupVisible tar gruppe-ID-en og et flagg, der én betyr synlig og null betyr skjult, og styrer standard skjermtilstand. SetOptionalContentGroupPrintable tar gruppe-ID-en og et flagg for om laget sendes ut når dokumentet skrives ut. De matchende getterne, GetOptionalContentGroupVisible og GetOptionalContentGroupPrintable, returnerer hver én eller null, slik at du kan lese tilbake et lags skjerm- og utskriftsinnstilling separat i stedet for å utlede den ene fra den andre.

Å bygge to lag på en side

Det å opprette et lag og fylle det følger en fast rekkefølge. Du tegner innholdet for laget på den gjeldende siden, og kaller deretter SetContentStreamOptional med gruppe-ID-en, som pakker inn sidens gjeldende innholdsstrøm slik at alt som er tegnet så langt tilhører den gruppen. Fordi kallet fanger opp det som er på strømmen i det øyeblikket, er regelen å legge ned ett lags merker, tildele dem, og først deretter starte på det neste laget. Eksempelet nedenfor plasserer verktøy (utilities) på den første siden og en evaluators rødlinje (redline) på en andre side, setter hvert lags skjerm- og utskriftstilstand, og lagrer.

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;

Rødlinje-laget er tilfellet som er verdt å merke seg. Det vises på skjermen slik at en evaluator ser merknaden, og utskriftsflagget er null slik at en utskrift av den samme filen ikke inneholder noen evalueringstekst. Denne asymmetrien er hele poenget med å holde de to tilstandene adskilt.

Lese konfigurasjonen tilbake

Å lese lag er en annen vandring gjennom den samme strukturen. Etter at en fil er lastet inn, rapporterer GetOptionalContentConfigCount hvor mange konfigurasjonsordbøker dokumentet inneholder; den første standardkonfigurasjonen er konfigurasjons-ID 1. Innenfor en konfigurasjon gir GetOptionalContentConfigOrderCount antall oppføringer i rekkefølgetreet, og du indekserer dem fra 1. For hver oppføring returnerer GetOptionalContentConfigOrderItemLabel dens visningstekst, og GetOptionalContentConfigOrderItemLevel returnerer dens nøstingsdybde, slik at en paneloversikt med underlag rykket inn under overskrifter kan rekonstrueres ordrett.

Hver oppføring har også en type. GetOptionalContentConfigOrderItemType skiller en faktisk valgfri innholdsgruppe fra en ren tekstetikett som bare eksisterer for å lede en seksjon av treet. Denne forskjellen er viktig fordi tilstandsspørringer per gruppe bare gir mening for reelle grupper. For en gruppeoppføring rapporterer GetOptionalContentConfigState om konfigurasjonen starter den på, av, eller lar den være uendret, og GetOptionalContentConfigLocked rapporterer om brukeren er forhindret fra å endre den. Løkken nedenfor gjengir rekkefølgetreet med hver gruppes tilstand og låsestatus, innrykket etter nivå.

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økken korrekt. Rekkefølgeindeksen er én-basert, fra 1 til antallet, noe som samsvarer med hvordan biblioteket nummererer treet internt. Og kallene per gruppe kjører bare når elementtypen er en gruppe, fordi en tekstetikett er en overskrift med et navn og et nivå, men har ingen på-, av- eller låst-tilstand å spørre etter. Hopper du over denne sjekken, ber du en etikett om en tilstand den ikke har.

Hvor dette passer inn

Lag er en presentasjonsmekanisme, så motoren må respektere dem på hver sti som gjengir en side, og gjengivelsessiden er dekket i vår gjennomgang av flermotors gjengivelse i Delphi. De krysser også med dokumentstruktur, fordi et lags navn er tekst rettet mot forfatteren og en leser drar nytte av en strukturert lagoversikt, som kobles til arbeidet i vår artikkel om tagget PDF og tilgjengelighetsstruktur. Begge pares med API-ene for valgfritt innhold beskrevet her, som leveres som en del av Delphi PDF Library sammen med side-, tekst-, font- og samsvarsfunksjonene som diskuteres andre steder på denne bloggen.