PDFium Component skapar nya PDF-lager från Delphi via TPdf.SaveAsOptionalContentCreated. I ett och samma verifierade sparande lägger metoden till optional content-grupper (OCG:er), optional content-medlemskapsordböcker (OCMD:er) och anroparlevererad ritning wrappad i Form XObjects vars /OC-post pekar på en ny grupp, så att PDFium och andra visare visar eller döljer den ritningen när lagret växlas
Förfrågan är sällan abstrakt. Ett anläggningsritningsteam vill ha metriska och imperialiska mått på samma ritningsblad, med exakt en uppsättning synlig. Ett tryckeri vill ha ett skärlinjeöverdrag som syns på skärmen men kan stängas av före korrekturet. Ingen av filerna har några lager ännu, så det finns ingenting att växla. Att växla befintliga lager är ett annat jobb, täckt i att växla befintliga PDF optional content-lager med SaveAsOptionalContentConfigured; den här artikeln handlar om att skapa lagren från början
Varför kan du inte bara sätta /OC på ett sidobjekt med PDFium?
PDFium saknar en publik funktion för att fästa optional content på ett befintligt sidobjekt. De publika rubrikerna innehåller ingen FPDFPageObj_SetOC eller något motsvarande, så den vanliga redigera-sedan-FPDFPage_GenerateContent-vägen kan inte producera styrt innehåll. Porten måste skrivas in i filstrukturen, och den enda frågan är vilken struktur PDFium hedrar när det renderar
ISO 32000-1 §8.11.3 ger tre sätt att göra grafik valfri, och PDFium hanterar dem olika:
- Avsnitt i sidinnehållsströmmar wrappade i markerat innehåll
/OC /Name BDC...EMC, kontrollerat per sidobjekt under renderingen - Form XObjects och bild-XObjects som bär en
/OC-post i sin egen ordbok, kontrollerade när XObject:et målas - Annoteringar med en
/OC-post i annoteringsordboken, som PDFium:s renderingsväg inte alls konsulterar, så en styrd annotering förblir synlig oavsett lagerläget
Markera-innehåll-inpackning betyder att skriva om befintliga innehållsströmmar operator för operator, och annoteringar utesluts av renderaren. Det lämnar Form XObject-vägen, och det är den PDFium Component använder: din ritning hamnar i ett nytt Form XObject med /OC pekande på gruppen, formen registreras i sidans /Resources /XObject-ordbok, och en ny innehållsström som håller en enda /Name Do fogas till sidans /Contents. Det befintliga sidinnehållet parsas eller skrivs aldrig om
Att beskriva grupper och styrda former i Delphi
TPdf.SaveAsOptionalContentCreated tar tre arrayer och returnerar en rapport. Båda overloaderna, en som skriver till en TStream och en till ett filnamn, delar samma parameterlista: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport, och en valfri AllowSignedDocument: Boolean = False. Metoden anlände i PDFium Component v3.154.0
Varje TPdfOptionalContentCreateGroup har en Name, en valfri Creator, en Intent av pociView eller pociDesign, en flagga InitialStateVisible, en flagga Locked och en RadioTag. Grupper som delar samma RadioTag landar i en /RBGroups-array, vilket är hur en visare vet att att slå på den ena slår av de andra. Varje TPdfOptionalContentCreateForm namnger sin styrande grupp via GroupIndex (ett nollbaserat index i din Groups-array), ett 1-baserat PageNumber, de råa innehållsströmsbytena, en begränsningsruta, en valfri matris och ett valfritt ResourceName
var
Pdf: TPdf;
Groups: TPdfOptionalContentCreateGroups;
Forms: TPdfOptionalContentCreateForms;
Report: TPdfOptionalContentCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'sheet-a1.pdf';
Pdf.Active := True;
SetLength(Groups, 2); // SetLength nollfyller posterna
Groups[0].Name := 'Dimensions (mm)';
Groups[0].Intent := pociView;
Groups[0].InitialStateVisible := True;
Groups[0].RadioTag := 'Units';
Groups[1].Name := 'Dimensions (in)';
Groups[1].Intent := pociView;
Groups[1].InitialStateVisible := False;
Groups[1].RadioTag := 'Units';
SetLength(Forms, 2);
Forms[0].GroupIndex := 0;
Forms[0].PageNumber := 1;
Forms[0].Content := TEncoding.ASCII.GetBytes(
'0 0 1 RG 1.5 w 72 120 m 540 120 l S');
Forms[0].BBoxRight := 612;
Forms[0].BBoxTop := 792;
Forms[0].ResourceName := 'DimMetric';
Forms[1] := Forms[0];
Forms[1].GroupIndex := 1;
Forms[1].Content := TEncoding.ASCII.GetBytes(
'1 0 0 RG 1.5 w 72 140 m 540 140 l S');
Forms[1].ResourceName := 'DimImperial';
if not Pdf.SaveAsOptionalContentCreated('sheet-a1-layers.pdf',
Groups, nil, Forms, Report) then
raise Exception.Create(Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Notera att formulärets innehåll är ren vektorsritning. Komponenten skriver bara en tom /Resources-ordbok på varje form, så innehållsströmmen måste vara självförsörjande: enhetsfärger, banor, klippning och transformeringar fungerar, men allt som behöver en namngiven resurs (ett typsnitt för Tf, en bild för Do, en ExtGState för gs) har inget att slå upp mot. Lämnar du ResourceName tomt namns formulären FmOc1, FmOc2 och så vidare; ett anpassat namn får bara innehålla bokstäver, siffror, understrykning, minus och punkt
Vad lägger en OCMD till över en vanlig grupp?
En optional content-medlemskapsordbok gör synligheten beroende av flera grupper på en gång. ISO 32000-1 §8.11.2.2 definierar två former: en /OCs-lista med en /P-policy som /AnyOn eller /AllOn, och ett /VE-synlighetsuttryck byggt av /And, /Or och /Not i specifikationen. PDFium Component tar en TPdfOptionalContentCreateMembership med MemberGroupIndices, en Policy och en Expression; när Expression.GroupIndices inte är tomt vinner uttrycket och /VE skrivs jämte /OCs
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Synlig när någon av enhetslagren är på: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Synlig bara medan det imperiska lagret är av: /VE [ /Not ref ]
SetLength(Memberships[1].MemberGroupIndices, 1);
Memberships[1].MemberGroupIndices[0] := 1;
Memberships[1].Expression.Kind := poceNot;
SetLength(Memberships[1].Expression.GroupIndices, 1);
Memberships[1].Expression.GroupIndices[0] := 1;
end;
Den stödda delmängden är medvetet smal. Policyer begränsas till ocmpAllOn och ocmpAnyOn; ocmpAnyOff och ocmpAllOff avvisas. Uttryck är en nivå djupa, med poceAll, poceAny eller poceNot över skapade grupper, och /Not kräver exakt ett grupindex. poceAll serialiseras som /And, poceAny som /Or och poceNot som /Not, de enda tre operatorer §8.11.2.2 definierar. Versioner före 3.181.2 skrev /All och /Any, vilka PDFium utvärderar som dolda, så har du skapat OCMD:er med ett äldre bygge, återskapa dem i stället för att lita på uttrycket. Varje medlemskap behöver också minst en post i MemberGroupIndices, även när ett uttryck finns, eftersom den listan blir /OCs-arrayen. Objektnumren för de nya ordböckerna kommer tillbaka i Report.CreatedMembershipObjectNumbers
Två beteenden är värda att känna innan du bygger vidare på OCMD:er. Först refererar de styrda formerna i samma anrop bara grupper: TPdfOptionalContentCreateForm.GroupIndex indexerar Groups, inte Memberships, så en form kan inte styras av en nyskapad OCMD via detta API. För det andra lägger PDFium Component till varje ny OCMD i katalogens /OCProperties /OCGs-array, medan /ON, /OFF och /Order förblir gruppenbart. ISO 32000-1 §8.11.4.2 beskriver /OCGs som listan av grupper, men utan den registreringen räknar TPdf.InspectOptionalContent inte alls upp medlemskapet. Bieffekten är att en inventarie av utdata listar varje OCMD som en extra namnlös post i Inventory.Groups, så matcha grupper efter Name i stället för efter position
Hur lappar komponenten en befintlig /OCProperties?
TPdf.SaveAsOptionalContentCreated bygger antingen /OCProperties från grunden eller utökar den som redan finns i katalogen, och den utökar genom att lägga till, aldrig genom att bygga om. Ett dokument utan lager får en ny ordbok med /OCGs och en standardkonfiguration /D som bär /ON, /OFF och /Order, plus /Locked och /RBGroups när någon grupp efterfrågar dem, som beskrivs i ISO 32000-1 §8.11.4.3
För ett dokument som redan har lager lappas varje målarray genom att de nya referenserna fogas in precis före dess avslutande hakparentes. Det låter petigt, och det är där av en anledning. En /Order-array kan hålla nästlade arrayer och textetiketter som formar lagretträdet en visare visar, och ett tidigare angreppssätt som plattade ut och byggde om arrayen tappade de etiketterna. Insättning i slutet behåller det befintliga trädet intakt och listar bara de nya grupperna efter det. Arrayer som bor i egna indirekta objekt löses upp och lappas på plats
Sedan v3.175.0 får också standardkonfigurationen i sig vara indirekt. Vissa producenter skriver /OCProperties << /OCGs [...] /D 12 0 R >>, och PDFium Component följer nu den referensen och lappar mål-ordboken i stället för att misslyckas med ett felaktig-post-fel. Direkta och indirekta sid-/Resources-ordböcker, inklusive en indirekt /XObject-underordbok, hanteras på samma sätt
Att bevisa att de nya lagren verkligen växlar
Ett lager som finns i /OCProperties men inte styr någonting är lätt att producera och svårt att märka. Under utvecklingen wrappade den första bygget av form-skrivaren ett strömobjekt som redan bar sin egen N 0 obj-rubrik och endobj-trailer inuti en andra rubrik. PDFium kunde inte parsa det fördubblade objektet, och i stället för att rapportera ett fel hoppade det tyst över Do-operatorn. Filen öppnades, lagerpanelen såg rätt ut, och den styrda ritningen dök aldrig upp. Det som blottade det var ett prob som växlade lagret och jämförde renderade pixlar: ingen skillnad, så något var fel
Det är varför metoden verifierar före leverans. Utdata återöppnas genom en strikt inläsning som kontrollerar korsreferenstabellen, och varje begärd grupp läses tillbaka via InspectOptionalContent för att bekräfta sitt namn, sitt initiala läge och sin låsregistrering. Först därefter kopieras utdata till din ström, och filoverloaden skriver en temporär fil och ersätter målet atomärt. Du kan köra samma växelkontroll själv genom att återöppna utdata, hitta gruppen efter namn och spara en kopia med motsatt läge
function SaveImperialPreset(const SourceName, TargetName: string): Boolean;
var
Pdf: TPdf;
Inventory: TPdfOptionalContentInventory;
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
CfgReport: TPdfOptionalContentConfigureReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourceName;
Pdf.Active := True;
if not Pdf.InspectOptionalContent(Inventory) then
raise Exception.Create(Inventory.ErrorMessage);
for I := 0 to High(Inventory.Groups) do
if (Inventory.Groups[I].Name = 'Dimensions (mm)') or
(Inventory.Groups[I].Name = 'Dimensions (in)') then
begin
SetLength(Changes, Length(Changes) + 1);
Changes[High(Changes)].GroupObjectNumber := Inventory.Groups[I].ObjectNumber;
Changes[High(Changes)].Visible := Inventory.Groups[I].Name = 'Dimensions (in)';
end;
Options := TPdfOptionalContentConfigureOptions.Default;
Result := Pdf.SaveAsOptionalContentConfigured(TargetName, Changes,
Options, CfgReport);
finally
Pdf.Free;
end;
end;
Rendera sida ett av båda filerna och de blå och röda linjerna ska byta plats. Gör de inte det, granska först gruppens EffectiveVisible-värde i inventarien; det speglar läget PDFium faktiskt kommer att rendera efter att konfigurationsarrayer och användningsregler applicerats
Gränser och fällor att planera kring
TPdf.SaveAsOptionalContentCreated är en avgränsad profil, och de flesta överraskningar kommer från att behandla den som en allmän lagereditor. Begränsningarna nedan tvingas fram i förväg, så ett regelbrytande anrop misslyckas med Report.ErrorMessage satt och skriver ingen utdata:
- Minst en grupp krävs; grupper, medlemskap och former är kapade vid 4096 poster vardera, gruppnamn måste vara unika och 1 till 255 tecken långa, och varje form accepterar högst 16 MiB innehållsbyte
- Krypterade dokument avvisas, och ett signerat dokument behöver
AllowSignedDocument = True; utdata är en fullständig omskrivning, så befintliga signaturer kommer inte längre att validera - Varje
Forms-post blir sitt eget Form XObject, så att lägga samma överdrag på 200 sidor lagrar innehållet 200 gånger i stället för en - Den nya
Do-strömmen fogas till efter befintligt sidinnehåll, så den körs i vilket grafiktillstånd de tidigare strömmarna lämnar efter sig; välformade sidor återställer sitt tillstånd, men en sida med obalanseradeq/Qkan flytta ditt överdrag - Befintligt sidinnehåll flyttas aldrig in i ett lager; bara ritningen du skickar i
Formsstyrs
Repetitionspunkten är den att väga för stora dokument. Är överdraget tungt och upprepas på varje sida håller delat-objekt-angreppssättet i återanvändbara sidstämplar byggda på Form XObjects en enda kopia i filen, till priset av att hantera lagerporten själv
FAQ
Kan PDFium Component lägga befintligt sidinnehåll i ett nytt lager?
Nej. TPdf.SaveAsOptionalContentCreated styr bara innehållet du levererar i Forms; den wrappar inte befintliga operatorer i markerat innehåll eller flyttar dem in i ett XObject. För att lägga befintlig grafik i lager, återskapa den som självförsörjande forminnehåll och ta bort den ursprungliga ritningen i ett separat steg
Varför är mina styrda annoteringar fortfarande synliga när lagret är av?
PDFium kontrollerar inte /OC-posten hos annoteringsordböcker på sin renderingsväg, så annoteringsnivåstyrning har ingen effekt i PDFium-baserade visare. Det är skälet PDFium Component fäster lagerinnehåll genom Form XObjects, som PDFium faktiskt styr
Vilka objektnummer ska jag använda när jag växlar de nya lagren senare?
Report.CreatedGroupObjectNumbers håller numren i filen som just skrevs. När du återöppnar den filen och konfigurerar den, ta ObjectNumber från TPdf.InspectOptionalContent och matcha efter Name, vilket förblir korrekt även om ett senare sparande omnumrerar objekt
PDFium Component levererar SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured och InspectOptionalContent som en enda optional content-verktygslåda för Delphi och C++Builder, byggd på PDFium-renderaren. Detaljer och nedladdningar finns på produktsidan för PDFium Component