PDFium Component maakt nieuwe PDF-lagen vanuit Delphi via TPdf.SaveAsOptionalContentCreated. In één geverifieerde save voegt de methode optional content groups (OCGs), optional content membership dictionaries (OCMDs) en door de aanroeper aangeleverde tekening toe, verpakt in Form XObjects waarvan de /OC-entry naar een nieuwe groep wijst, zodat PDFium en andere viewers die tekening tonen of verbergen wanneer de laag wordt omgezet
Het verzoek is zelden abstract. Een installatietechniekteam wil metrische en imperiale maatvoering op hetzelfde tekenblad, met precies één set zichtbaar. Een drukkerij wil een snijlijn-overlay die op het scherm verschijnt maar vóór de proef kan worden uitgezet. Geen van beide bestanden heeft al lagen, dus er is niets om om te zetten. Bestaande lagen omschakelen is een andere klus, behandeld in bestaande PDF optional content-lagen omschakelen met SaveAsOptionalContentConfigured; dit artikel gaat over de lagen er in de eerste plaats maken
Waarom kunt u niet gewoon /OC op een page object zetten met PDFium?
PDFium heeft geen publieke functie om optional content aan een bestaand page object te hangen. De publieke headers bevatten geen FPDFPageObj_SetOC of iets equivalent, dus de gebruikelijke route van bewerken en dan FPDFPage_GenerateContent kan geen gegate content opleveren. De gate moet in de bestandsstructuur worden geschreven, en de enige vraag is welke structuur PDFium eert wanneer hij rendert
ISO 32000-1 §8.11.3 geeft drie manieren om graphics optioneel te maken, en PDFium behandelt ze verschillend:
- Secties van de pagina content stream gewikkeld in marked content
/OC /Name BDC...EMC, gecontroleerd per page object tijdens het renderen - Form XObjects en image XObjects die een
/OC-entry in hun eigen dictionary dragen, gecontroleerd wanneer het XObject wordt geschilderd - Annotaties met een
/OC-entry in de annotatiedictionary, die het PDFium-renderpad helemaal niet raadpleegt, dus een gegate annotatie blijft zichtbaar ongeacht de laagstatus
Marked-content-wrapping betekent bestaande content streams herschrijven operator voor operator, en annotaties worden door de renderer uitgesloten. Dat laat de Form XObject-route over, en dat is de route die PDFium Component gebruikt: uw tekening gaat in een nieuw Form XObject met /OC naar de groep wijzend, het form wordt geregistreerd in de dictionary /Resources /XObject van de pagina, en een nieuwe content stream met een enkele /Name Do wordt aan de pagina-/Contents toegevoegd. De bestaande pagina-inhoud wordt nooit geparsed of herschreven
Groepen en gegate forms in Delphi beschrijven
TPdf.SaveAsOptionalContentCreated neemt drie arrays aan en geeft een rapport terug. Beide overloads, één die naar een TStream schrijft en één naar een bestandsnaam, delen dezelfde parameterlijst: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport, en een optionele AllowSignedDocument: Boolean = False. De methode verscheen in PDFium Component v3.154.0
Elk TPdfOptionalContentCreateGroup heeft een Name, een optionele Creator, een Intent van pociView of pociDesign, een vlag InitialStateVisible, een vlag Locked en een RadioTag. Groepen die dezelfde RadioTag delen belanden in één array /RBGroups, en daaraan ziet een viewer dat het aanzetten van de ene de anderen uitzet. Elk TPdfOptionalContentCreateForm benoemt zijn gating-groep bij GroupIndex (een 0-based index in uw array Groups), een 1-based PageNumber, de rauwe content stream-bytes, een bounding box, een optionele matrix en een optionele 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 vult de records met nullen
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;
Merk op dat de form-inhoud pure vectortekening is. De component schrijft alleen een lege /Resources-dictionary op elk form, dus de content stream moet zelfvoorzienend zijn: device-kleuren, paths, clipping en transformaties werken, maar alles wat een benoemde resource nodig heeft (een font voor Tf, een image voor Do, een ExtGState voor gs) heeft niets om tegen te resolven. Laat u ResourceName leeg, dan heten de forms FmOc1, FmOc2 enzovoort; een custom naam mag alleen letters, cijfers, underscore, min en punt bevatten
Wat voegt een OCMD toe boven een kale groep?
Een optional content membership dictionary laat zichtbaarheid van verscheidene groepen tegelijk afhangen. ISO 32000-1 §8.11.2.2 definieert twee vormen: een lijst /OCs met een policy /P zoals /AnyOn of /AllOn, en een visibility expression /VE opgebouwd uit /And, /Or en /Not in de specificatie. PDFium Component neemt een TPdfOptionalContentCreateMembership aan met MemberGroupIndices, een Policy en een Expression; is Expression.GroupIndices niet leeg, dan wint de expressie en wordt /VE naast /OCs geschreven
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Zichtbaar wanneer een van beide unit-lagen aan staat: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Zichtbaar alleen zolang de imperiale laag uit staat: /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;
De ondersteunde subset is bewust smal. Policies zijn beperkt tot ocmpAllOn en ocmpAnyOn; ocmpAnyOff en ocmpAllOff worden geweigerd. Expressies zijn één niveau diep, met poceAll, poceAny of poceNot over aangemaakte groepen, en /Not vereist exact één groepsindex. poceAll serialiseert als /And, poceAny als /Or en poceNot als /Not, de enige drie operators die §8.11.2.2 definieert. Versies vóór 3.181.2 schreven /All en /Any, die PDFium als verborgen evalueert, dus heeft u OCMDs met een oudere build aangemaakt, genereer ze opnieuw in plaats van de expressie te vertrouwen. Elk lidmaatschap heeft ook ten minste één entry in MemberGroupIndices nodig, ook wanneer een expressie aanwezig is, want die lijst wordt de array /OCs. De objectnummers van de nieuwe dictionaries komen terug in Report.CreatedMembershipObjectNumbers
Twee gedragingen zijn het kennen waard voordat u op OCMDs bouwt. Ten eerste verwijzen de gegate forms in dezelfde aanroep alleen naar groepen: TPdfOptionalContentCreateForm.GroupIndex indexeert Groups, niet Memberships, dus een form kan niet via deze API door een pas aangemaakte OCMD worden gegate. Ten tweede voegt PDFium Component elke nieuwe OCMD toe aan de array /OCProperties /OCGs van de catalogus, terwijl /ON, /OFF en /Order alleen-groepen blijven. ISO 32000-1 §8.11.4.2 beschrijft /OCGs als de lijst van groepen, maar zonder die registratie sommereert TPdf.InspectOptionalContent het lidmaatschap helemaal niet. Het bijeffect is dat een inventaris van de output elke OCMD als een extra naamloze entry in Inventory.Groups vermeldt, dus match groepen op Name in plaats van op positie
Hoe patched de component een bestaande /OCProperties?
TPdf.SaveAsOptionalContentCreated bouwt /OCProperties óf vanaf nul óf breidt degene die al in de catalogus zit uit, en hij breidt uit door toe te voegen, nooit door te herbouwen. Een document zonder lagen krijgt een nieuwe dictionary met /OCGs en een defaultconfiguratie /D die /ON, /OFF en /Order draagt, plus /Locked en /RBGroups wanneer enige groep daarom vraagt, zoals uiteengezet in ISO 32000-1 §8.11.4.3
Voor een document dat al lagen heeft wordt elke doelarray gepatched door de nieuwe referenties vlak vóór zijn sluitende haak in te voegen. Dat klinkt pietluttig, en het staat er met reden. Een array /Order kan geneste arrays en tekstlabels bevatten die de laagboom vormgeven die een viewer toont, en een eerdere aanpak die de array platte en herbouwde verloor die labels. Invoegen op het eind houdt de bestaande boom intact en sommereert simpelweg de nieuwe groepen erna. Arrays die in hun eigen indirecte objecten wonen worden op hun plek geresolvd en gepatched
Sinds v3.175.0 mag ook de defaultconfiguratie zelf indirect zijn. Sommige producenten schrijven /OCProperties << /OCGs [...] /D 12 0 R >>, en PDFium Component volgt die referentie nu en patcht de doeldictionary in plaats van te falen met een misvormde-entry-fout. Directe en indirecte pagina-/Resources-dictionaries, inbegrepen een indirecte subdictionary /XObject, worden op dezelfde manier behandeld
Bewijzen dat de nieuwe lagen werkelijk omschakelen
Een laag die in /OCProperties bestaat maar niets gate is makkelijk te produceren en moeilijk op te merken. Tijdens de ontwikkeling wikkelde de eerste build van de form-writer een stream object dat al zijn eigen header N 0 obj en trailer endobj droeg in een tweede header. PDFium kon het verdubbelde object niet parsen, en in plaats van een fout te melden sloeg hij zwijgend de operator Do over. Het bestand opende, het lagenpaneel zag er goed uit, en de gegate tekening verscheen nooit. Wat hem ontmaskerde was een probe die de laag omschakelde en gerenderde pixels vergeleek: geen verschil, dus er klopte iets niet
Daarom verifieert de methode vóór het opleveren. De output wordt heropend via een strenge load die de cross-reference tabel controleert, en elke gevraagde groep wordt via InspectOptionalContent teruggelezen om zijn naam, zijn begintoestand en zijn locked-registratie te bevestigen. Pas daarna wordt de output naar uw stream gekopieerd, en de file-overload schrijft een tijdelijk bestand en vervangt het doel atomair. Dezelfde omschakelcontrole kunt u zelf draaien door de output te heropenen, de groep bij naam te vinden en een kopie met de tegenovergestelde toestand op te slaan
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;
Render pagina één van beide bestanden en de blauwe en rode lijnen zouden moeten wisselen. Doen ze dat niet, inspecteer dan eerst de waarde EffectiveVisible van de groep in de inventaris; hij weerspiegelt de toestand die PDFium werkelijk rendert nadat configuratiearrays en usage rules zijn toegepast
Grenzen en valkuilen om omheen te plannen
TPdf.SaveAsOptionalContentCreated is een begrensd profiel, en de meeste verrassingen komen van hem als algemene lageneditor behandelen. De beperkingen hieronder worden vooraf gehandhaafd, dus een overtredende aanroep faalt met Report.ErrorMessage gezet en schrijft geen output:
- Ten minste één groep is vereist; groepen, lidmaatschappen en forms zijn elk gekapt op 4096 entries, groepsnamen moeten uniek zijn en 1 tot 255 tekens lang, en elke form accepteert ten hoogste 16 MiB aan contentbytes
- Versleutelde documenten worden geweigerd, en een ondertekend document heeft
AllowSignedDocument = Truenodig; de output is een volledige herschrijving, dus bestaande handtekeningen valideren niet meer - Elke
Forms-entry wordt zijn eigen Form XObject, dus dezelfde overlay op 200 pagina's zetten bewaart de content 200 keer in plaats van één keer - De nieuwe
Do-stream wordt na de bestaande pagina-inhoud toegevoegd, dus hij draait in welke graphics state de eerdere streams ook achterlaten; goed gevormde pagina's herstellen hun toestand, maar een pagina met uit balans zijndeq/Qkan uw overlay verschuiven - Bestaande pagina-inhoud wordt nooit in een laag verplaatst; alleen de tekening die u in
Formsmeegeeft wordt gegate
Het herhalingspunt is het afwegen waard voor grote documenten. Is de overlay zwaar en herhaalt hij zich op elke pagina, dan houdt de gedeelde-object-aanpak in herbruikbare page stamps op Form XObjects één kopie in het bestand, tegen de kost van de laag-gate zelf af te handelen
FAQ
Kan PDFium Component bestaande pagina-inhoud in een nieuwe laag zetten?
Nee. TPdf.SaveAsOptionalContentCreated gate alleen de content die u in Forms aanlevert; hij wikkelt bestaande operators niet in marked content en verplaatst ze niet naar een XObject. Om bestaand artwork in lagen te verdelen, genereer het opnieuw als zelfvoorzienende form-content en verwijder de originele tekening in een aparte stap
Waarom zijn mijn gegate annotaties nog steeds zichtbaar wanneer de laag uit staat?
PDFium controleert de /OC-entry van annotatiedictionaries niet op zijn renderpad, dus gating op annotatieniveau heeft geen effect in op PDFium gebaseerde viewers. Dat is de reden dat PDFium Component laagcontent aan Form XObjects hangt, die PDFium wel gate
Welke objectnummers moet ik gebruiken wanneer ik de nieuwe lagen later omschakel?
Report.CreatedGroupObjectNumbers bevat de nummers in het bestand dat net is geschreven. Opent u dat bestand opnieuw en configureert het, neem dan ObjectNumber uit TPdf.InspectOptionalContent en match op Name, wat correct blijft ook wanneer een latere save objecten hernummert
PDFium Component levert SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured en InspectOptionalContent als één optional content-toolkit voor Delphi en C++Builder, gebouwd op de PDFium-renderer. Details en downloads staan op de PDFium Component productpagina