Teknisk artikel

PDF-lager i Delphi: Alternativa innehållsgrupper (OCG)

En lantmätare (surveyor) öppnar en situationsplan (site plan) och vill att höjdkurvorna (contours) ska vara dolda medan ledningarna (utilities) förblir på. En granskare vill att de röda markeringarna (redline annotations) ska vara synliga på skärmen och borta från utskriften. Ett produktblad skeppas på tre språk från en enda fil, och läsaren väljer vilket språk som visas. Alla tre är samma PDF-funktion, och den panel som driver dem i Acrobat kallas Lager (Layers). Funktionen bakom (underneath) den panelen är alternativt innehåll (optional content), och det är vad som låter en enskild sida bära flera oberoende visuella skikt (strata) som en visare kan slå på och av

Alternativt innehåll specificeras i ISO 32000-1 §8.11. Enheten för synlighet (visibility) är en alternativ innehållsgrupp (optional content group), en OCG, ett lexikon (dictionary) av typen /OCG som bär ett namn. Markerat innehåll (Marked content) på en sida associeras med en grupp, och visaren beslutar om den gruppen för närvarande visas. En besläktad konstruktion, medlemskapslexikonet för alternativt innehåll (optional content membership dictionary) eller OCMD, låter synlighet bero på en boolesk kombination av flera grupper, men det vardagliga fallet (everyday case) är en enskild namngiven grupp som står för ett enskilt lager. Dokumentet knyter samman (ties together) hela mekanismen genom en enda katalogpost (catalog entry), /OCProperties, vilken beskrivs härnäst

Vad katalogen måste bära (carry)

En OCG i sig (on its own) är inaktiv (inert). För att en visare ska kunna lista ett lager och minnas dess tillstånd behöver dokumentkatalogen (document catalog) ett /OCProperties-lexikon, och §8.11.4 slår fast (lays out) exakt vad som ingår i det. Det finns en /OCGs-array som namnger varje grupp i filen, och det finns en /D-post som håller standardkonfigurationen (default configuration). Standardkonfigurationen är den del en läsare (reader) applicerar när filen öppnas första gången. Den registrerar vilka grupper som startar på och vilka som startar av, vilka poster som är låsta (locked) från att användaren växlar dem (toggling them), och, via en /Order-array, hur lagernamnen arrangeras och nästlas (nested) i panelen

Den praktiska konsekvensen är att skapandet av ett lager aldrig är en rent lokal handling. Gruppen måste ritas in på (drawn into on) sidan, och den måste också registreras i en struktur på katalognivå som tidigare inte existerade. PDFlibPas gör båda åt dig. Det första anropet som gör en grupp lägger till /OCProperties-posten i katalogen och grundlägger (seeds) standardkonfigurationen, så lagret blir både målat (painted) och listat utan separat bokföring (bookkeeping) från din sida

Varför ett efterlevnadsläge (compliance mode) kan undanhålla (withhold) funktionen

Innan någon lagerkod körs avgör (decides) dokumentets efterlevnadsmål (conformance target) huruvida alternativt innehåll ens är lagligt. PDF/A-1, arkivprofilen definierad i ISO 19005-1, förbjuder (forbids) /OCProperties-posten rakt av (outright) i §6.1.13. Resonemanget passar formatets syfte. En arkivfil måste renderas identiskt för varje läsare långt in i framtiden, och innehåll vars synlighet en visare kan ändra är innehåll vars utseende inte är fixerat, så profilen bannlyser (bans) konstruktionen snarare än att tillåta ett tvetydigt arkiv (ambiguous archive). PDF/A-2 och PDF/A-3, definierade i ISO 19005-2 och ISO 19005-3, intar (take) den motsatta ståndpunkten i deras §6.9 och tillåter alternativt innehåll, med regler kring standard-synlighet

Den skillnaden visar sig direkt i API:et. När dokumentet är i ett PDF/A-1-läge vägrar (refuses) NewOptionalContentGroup att skapa gruppen och returnerar noll, eftersom ett tillmötesgående (honouring) av begäran skulle producera en fil som fallerar sin egen deklarerade efterlevnad. I PDF/A-2- eller PDF/A-3-läge, och i ordinär obegränsad PDF (unconstrained PDF), lyckas samma anrop och returnerar ett nollskilt (non-zero) grupp-ID. Ett nollresultat är därför inget generiskt fel att inspektera senare; det är biblioteket som berättar för dig att den aktiva efterlevnadsnivån inte har plats för 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;

Två tillstånd per lager, inte ett

Ett lager är inte helt enkelt (simply) synligt eller osynligt. Standardkonfigurationen registrerar dess tillstånd på skärmen (on-screen state) och ett separat utskriftstillstånd (print state), eftersom §8.11.4 skiljer på vad en visare visar från vad en utskrifts-pipeline genererar (emits). De två är oberoende med avsikt (on purpose). En utkast-vattenstämpel (draft watermark) kan visas på skärmen och utelämnas (dropped) från papper, och ett skärlinje-lager (cut-line layer) kan vara dolt på skärmen men ändå skickas till en plotter. Att slå ihop de två (Collapsing the two) skulle tvinga den ena att följa den andra och därmed förlora exakt den kontroll funktionen existerar för att ge

PDFlibPas exponerar paret genom två sättare (setters). SetOptionalContentGroupVisible tar grupp-ID:t och en flagga, där ett betyder synligt och noll betyder dolt, och styr standardtillståndet för skärm (default on-screen state). SetOptionalContentGroupPrintable tar grupp-ID:t och en flagga för huruvida lagret genereras (emitted) när dokumentet skrivs ut. De motsvarande hämtarna (matching getters), GetOptionalContentGroupVisible och GetOptionalContentGroupPrintable, returnerar vardera ett eller noll, så att du kan läsa tillbaka ett lagers skärm- och utskriftsdisposition separat (separately) snarare än att härleda (inferring) det ena från det andra

Att bygga två lager på en sida

Att skapa ett lager och fylla det följer en fast ordning (fixed order). Du ritar innehållet för lagret på den aktuella sidan, och anropar sedan SetContentStreamOptional med grupp-ID:t, vilket omsluter (wraps) sidans aktuella innehållsström (content stream) så att allt som ritats hittills tillhör den gruppen. Eftersom anropet fångar (captures) det som finns på strömmen vid det ögonblicket är disciplinen att lägga ned ett lagers märken (marks), tilldela (assign) dem, och först därefter starta nästa lager. Exemplet nedan lägger ledningar (utilities) på första sidan och granskarens röda markering (reviewer redline) på en andra sida, sätter varje lagers skärm- och utskriftstillstånd, och sparar

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ödmarkering-lagret (The redline layer) är fallet värt att notera. Det visas på skärmen så att en granskare ser anteckningen, och dess utskrivbarhetsflagga (printable flag) är noll så en utskrift av samma fil bär (carries) ingen granskningstext. Den asymmetrin är hela poängen med att hålla de två tillstånden åtskilda (apart)

Att läsa konfigurationen tillbaka

Att läsa lager är en annorlunda promenad (walk) genom samma struktur. Efter att en fil är laddad rapporterar GetOptionalContentConfigCount hur många konfigurationslexikon dokumentet rymmer (holds); den första standardkonfigurationen är konfig-ID 1. Inuti en konfiguration ger GetOptionalContentConfigOrderCount antalet poster (entries) i ordningsträdet (order tree), och du indexerar dem från 1. För varje post returnerar GetOptionalContentConfigOrderItemLabel dess visningstext (display text) och GetOptionalContentConfigOrderItemLevel returnerar dess nästlingsdjup, så att en panelstruktur (panel outline) med under-lager (sub-layers) indenterade under rubriker (headings) kan rekonstrueras ordagrant (verbatim)

Varje post har också en typ. GetOptionalContentConfigOrderItemType särskiljer (distinguishes) en faktisk alternativ innehållsgrupp (optional content group) från en vanlig textetikett (plain text label) som enbart existerar för att inleda (head) en sektion av trädet. Den distinktionen spelar roll eftersom de per-grupp-tillståndsfrågorna (per-group state queries) bara är meningsfulla (make sense) för riktiga grupper. För en grupp-post rapporterar GetOptionalContentConfigState huruvida konfigurationen startar den på, av, eller lämnar den oförändrad, och GetOptionalContentConfigLocked rapporterar huruvida användaren är utestängd (barred) från att växla den. Loopen nedan renderar ordningsträdet med varje grupps tillstånd och låsningsstatus (lock status), och indenterar efter nivå (by level)

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;

Två detaljer håller den här loopen korrekt. Ordningsindexet (The order index) är ett-baserat, från 1 till antalet, vilket matchar hur biblioteket numrerar trädet internt. Och per-grupp-anropen körs bara när post-typen (item type) är en grupp, eftersom en textetikett är en rubrik med ett namn och en nivå men inget på-, av- eller låst tillstånd att fråga efter (to query). Hoppar du över den spärren (guard) frågar du en etikett efter ett tillstånd den inte har

Var detta passar in (Where this fits)

Lager är en presentationsmekanism (presentation mechanism), så motorn måste hedra dem på varje sökväg som renderar en sida, och renderingssidan (rendering side) täcks in i vår genomgång (walkthrough) av multimotor-rendering i Delphi. De korsar (intersect) också dokumentstruktur, eftersom ett lagers namn är en författar-viktad text (author-facing text) och en läsare drar nytta av (benefits from) en strukturerad lagerkontur, vilket knyter an (connects) till arbetet i vår artikel om taggad PDF och tillgänglighetsstruktur. Båda parar (pair) sig med de API:er för alternativt innehåll (optional content APIs) som beskrivits här, och som skeppas som en del av Delphi PDF Library jämsides med sido-, text-, typsnitts- och efterlevnadsfaciliteterna (conformance facilities) som diskuteras på andra ställen i den här bloggen