En lantmätare öppnar en situationsplan och vill ha höjdkurvorna dolda medan ledningarna förblir synliga. En granskare vill ha markeringsannoteringar (redline annotations) synliga på skärmen men borta från utskriften. Ett produktblad levereras på tre språk i en och samma fil, och läsaren väljer vilket språk som visas. Alla tre är samma PDF-funktion, och panelen som styr dem i Acrobat kallas Lager. Funktionen bakom den panelen är valfritt innehåll (optional content), och det är det som gör att en enskild sida kan bära flera oberoende visuella skikt som ett visningsprogram slår på och av.
Valfritt innehåll specificeras i ISO 32000-1 §8.11. Synlighetsenheten är en valfri innehållsgrupp, en OCG (optional content group), en ordbok av typen /OCG som bär ett namn. Markeringsinnehåll på en sida är associerat med en grupp, och visningsprogrammet avgör om den gruppen för närvarande visas. En relaterad konstruktion, medlemskapsordboken för valfritt innehåll eller OCMD (optional content membership dictionary), låter synligheten bero på en boolesk kombination av flera grupper, men det vardagliga fallet är en enskild namngiven grupp som representerar ett enskilt lager. Dokumentet knyter samman hela mekanismen genom en katalogpost, /OCProperties, som beskrivs härnäst.
Vad katalogen måste innehålla
En OCG i sig själv är inaktiv. För att ett visningsprogram ska kunna lista ett lager och komma ihåg dess tillstånd behöver dokumentkatalogen en /OCProperties-ordbok, och §8.11.4 anger exakt vad som ingår i den. Det finns en /OCGs-array som namnger varje grupp i filen, och det finns en /D-post som innehåller standardkonfigurationen. Standardkonfigurationen är den del som läsaren tillämpar när filen öppnas första gången. Den registrerar vilka grupper som startar som aktiva och vilka som startar som inaktiva, vilka poster som är låsta mot att användaren växlar dem, och, via en /Order-array, hur lagernamnen är arrangerade och nästlade i panelen.
Den praktiska konsekvensen är att skapandet av ett lager aldrig är en rent lokal handling. Gruppen måste ritas på sidan, och den måste också registreras i en struktur på katalognivå som inte tidigare existerade. PDFlibPas gör båda åt dig. Det första anropet som skapar en grupp lägger till posten /OCProperties i katalogen och sätter standardkonfigurationen, så att lagret både ritas och listas utan att du behöver sköta någon separat bokföring.
Varför ett efterlevnadsläge kan blockera funktionen
Innan någon lagerkod körs avgör dokumentets mål för efterlevnad (conformance target) om valfritt innehåll överhuvudtaget är tillåtet. PDF/A-1, arkiveringsprofilen som definieras i ISO 19005-1, förbjuder posten /OCProperties helt och hållet i §6.1.13. Resonemanget ligger i formatets syfte. En arkivfil måste renderas identiskt för varje läsare långt in i framtiden, och innehåll vars synlighet en användare kan ändra är innehåll vars utseende inte är fast, så profilen förbjuder konstruktionen snarare än att tillåta ett tvetydigt arkiv. PDF/A-2 och PDF/A-3, som definieras i ISO 19005-2 och ISO 19005-3, har motsatt uppfattning i sin §6.9 och tillåter valfritt innehåll, med regler för standardsynlighet.
Den skillnaden visar sig direkt i API:et. När dokumentet är i ett PDF/A-1-läge vägrar NewOptionalContentGroup att skapa gruppen och returnerar noll, eftersom att uppfylla begäran skulle producera en fil som misslyckas med sin egen deklarerade efterlevnad. I PDF/A-2- eller PDF/A-3-läge, samt i vanliga obegränsade PDF-filer, lyckas samma anrop och returnerar ett grupp-ID som inte är noll. Ett nollresultat är därför inte ett allmänt fel att inspektera senare; det är biblioteket som talar om för dig att den aktiva efterlevnadsnivån inte tillåter 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 bara synligt eller osynligt. Standardkonfigurationen registrerar dess tillstånd på skärmen och ett separat utskriftstillstånd, eftersom §8.11.4 skiljer på vad ett visningsprogram visar och vad en utskriftskanal skickar ut. De två är medvetet oberoende av varandra. En utkast-vattenstämpel (draft watermark) kan visas på skärmen men tas bort på papper, och ett skärlinjelager (cut-line layer) kan döljas på skärmen men skickas till en plotter. Att slå samman de två skulle tvinga det ena att följa det andra och förlora exakt den kontroll som funktionen finns till för att ge.
PDFlibPas exponerar paret genom två skrivfunktioner (setters). SetOptionalContentGroupVisible tar grupp-ID:t och en flagga, där ett betyder synlig och noll betyder dold, och styr standardtillståndet på skärmen. SetOptionalContentGroupPrintable tar grupp-ID:t och en flagga för om lagret ska skickas med när dokumentet skrivs ut. De matchande läsfunktionerna (getters), GetOptionalContentGroupVisible and GetOptionalContentGroupPrintable, returnerar vardera ett eller noll, så att du kan läsa av lagrets skärm- och utskriftsstatus separat istället för att härleda den ena från den andra.
Att bygga två lager på en sida
Att skapa ett lager och fylla det följer en bestämd ordning. Du ritar innehållet för lagret på den aktuella sidan, anropar sedan SetContentStreamOptional med grupp-ID:t, vilket omsluter sidans aktuella innehållsström så att allt som ritats hittills tillhör den gruppen. Eftersom anropet fångar upp allt som finns i strömmen vid det ögonblicket, är regeln att rita ut ett lagers element, tilldela dem och först därefter påbörja nästa lager. Exemplet nedan lägger ledningar (utilities) på den första sidan och granskningsmarkeringar (reviewer redline) på en andra sida, ställer in varje lagers skärm- och utskriftstillstånd samt 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;
Granskningslagret (redline layer) är fallet som är värt att notera. Det visas på skärmen så att en granskare ser anteckningen, och dess utskriftsflagga är noll så att en utskrift av samma fil inte innehåller någon granskningstext. Den asymmetrin är hela poängen med att hålla isär de två tillstånden.
Att läsa tillbaka konfigurationen
Att läsa in lager är en annan vandring genom samma struktur. Efter att en fil har lästs in rapporterar GetOptionalContentConfigCount hur många konfigurationsordböcker dokumentet innehåller; den första standardkonfigurationen har konfigurations-ID 1. Inom en konfiguration ger GetOptionalContentConfigOrderCount antalet poster i ordningsträdet, och du indexerar dem från 1. För varje post returnerar GetOptionalContentConfigOrderItemLabel dess visningstext och GetOptionalContentConfigOrderItemLevel returnerar dess nästlingsdjup, så att en panelstruktur med underlager indragna under rubriker kan rekonstrueras ordagrant.
Varje post har också en typ. GetOptionalContentConfigOrderItemType skiljer en faktisk valfri innehållsgrupp (optional content group) från en vanlig textetikett som endast existerar som rubrik för en sektion av trädet. Den skillnaden är viktig eftersom tillståndsfrågor per grupp endast är meningsfulla för riktiga grupper. För en grupp-post rapporterar GetOptionalContentConfigState om konfigurationen startar den som aktiv, inaktiv eller lämnar den oförändrad, och GetOptionalContentConfigLocked rapporterar om användaren är blockerad från att ändra den. Loopen nedan renderar ordningsträdet med varje grupps tillstånd och låsstatus, indraget efter 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;
Två detaljer håller den här loopen korrekt. Ordningens index är ett-baserat, från 1 till antalet, vilket matchar hur biblioteket numrerar trädet internt. Och anropen per grupp körs endast när elementtypen är en grupp, eftersom en textetikett är en rubrik med ett namn och en nivå men utan något på-, av- eller låst tillstånd att fråga efter. Hoppa över det skyddet och du frågar en etikett efter ett tillstånd den inte har.
Där detta passar in
Lager är en presentationsmekanism, så motorn måste respektera dem på varje sökväg som renderar en sida, och renderingssidan täcks i vår genomgång av rendering med flera motorer i Delphi. De korsar också dokumentstrukturen, eftersom ett lagers namn är text som vänder sig till författaren och en läsare drar nytta av en strukturerad lagerstruktur, vilket hänger samman med arbetet i vår artikel om taggad PDF och tillgänglighetsstruktur. Båda paras ihop med API:erna för valfritt innehåll som beskrivs här, vilka levereras som en del av Delphi PDF Library tillsammans med funktionerna för sidor, text, typsnitt och efterlevnad som diskuteras på andra ställen i den här bloggen.