PDFium Component skaber nye PDF-lag fra Delphi gennem TPdf.SaveAsOptionalContentCreated. I én verificeret gemning tilføjer metoden optional content-grupper (OCG'er), optional content-medlemskabsdictionaries (OCMD'er) og kalder-leveret tegning pakket ind i Form XObjects, hvis /OC-indgang peger på en ny gruppe, så PDFium og andre viewers viser eller skjuler den tegning, når laget toggles
Anmodningen er sjældent abstrakt. Et anlægsingeniørteam vil have metriske og imperiale mål på samme tegneblad med præcis ét sæt synligt. Et trykkeri vil have en skærelinje-overlay, der vises på skærmen, men kan slås fra før korrekturen. Ingen af filerne har nogen lag endnu, så der er intet at toggle. At skifte eksisterende lag er et andet job, dækket i toggling af eksisterende PDF optional content-lag med SaveAsOptionalContentConfigured; denne artikel handler om at skabe lagene i første omgang
Hvorfor kan du ikke bare sætte /OC på et sideobjekt med PDFium?
PDFium har ingen offentlig funktion til at knytte optional content til et eksisterende sideobjekt. De offentlige headers indeholder ingen FPDFPageObj_SetOC eller noget ækvivalent, så den sædvanlige rediger-og-derefter-FPDFPage_GenerateContent-rute kan ikke producere gated indhold. Porten må skrives ind i filstrukturen, og det eneste spørgsmål er, hvilken struktur PDFium ærer, når den renderer
ISO 32000-1 §8.11.3 giver tre måder at gøre grafik valgfri på, og PDFium håndterer dem forskelligt:
- Sideindhold-streamsektioner pakket ind i
/OC /Name BDC...EMCmarked content, tjekket pr. sideobjekt under rendering - Form XObjects og image XObjects, der bærer en
/OC-indgang i deres egen dictionary, tjekket, når XObject'en males - Annotationer med en
/OC-indgang i annotationsdictionaryen, som PDFium's renderti slet ikke konsulterer, så en gated annotation forbliver synlig uanset lagtilstanden
Marked-content-indpakning betyder at omskrive eksisterende content streams operator for operator, og annotationer er udelukket af rendereren. Det efterlader Form XObject-ruten, og det er den, PDFium Component bruger: din tegning går ind i et nyt Form XObject med /OC pegende på gruppen, formen registreres i sidens /Resources /XObject-dictionary, og en ny content stream med et enkelt /Name Do tilføjes til sidens /Contents. Det eksisterende sideindhold parses eller omskrives aldrig
At beskrive grupper og gatede forms i Delphi
TPdf.SaveAsOptionalContentCreated tager tre arrays og returnerer en rapport. Begge overloads, én der skriver til en TStream og én til et filnavn, deler samme parameterliste: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport og en valgfri AllowSignedDocument: Boolean = False. Metoden ankom i PDFium Component v3.154.0
Hver TPdfOptionalContentCreateGroup har et Name, en valgfri Creator, en Intent af pociView eller pociDesign, et InitialStateVisible-flag, et Locked-flag og en RadioTag. Grupper, der deler samme RadioTag, lander i ét /RBGroups-array, hvilket er, hvordan en viewer ved, at at slå én til slår de andre fra. Hver TPdfOptionalContentCreateForm navngiver sin gating-gruppe ved GroupIndex (et nul-baseret indeks i dit Groups-array), et 1-baseret PageNumber, de rå content stream-bytes, en bounding box, en valgfri matrix og et valgfrit 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 nulfylder posterne
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;
Bemærk, at formindholdet er ren vektor-tegning. Komponenten skriver kun en tom /Resources-dictionary på hver form, så content streamen skal være selvstændig: device-farver, stier, clipping og transforms virker, men alt, der behøver en navngiven ressource (en font til Tf, et billede til Do, en ExtGState til gs) har intet at resolvere imod. Lader du ResourceName stå tom, navngives forms FmOc1, FmOc2 og så videre; et custom-navn må kun indeholde bogstaver, cifre, underscore, minus og punktum
Hvad tilføjer en OCMD over en almindelig gruppe?
En optional content-medlemskabsdictionary får visibility til at afhænge af flere grupper på én gang. ISO 32000-1 §8.11.2.2 definerer to former: en /OCs-liste med en /P-policy som /AnyOn eller /AllOn, og et /VE-visibility-udtryk bygget af /And, /Or og /Not i specifikationen. PDFium Component tager en TPdfOptionalContentCreateMembership med MemberGroupIndices, en Policy og en Expression; når Expression.GroupIndices ikke er tom, vinder udtrykket, og /VE skrives ved siden af /OCs
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Synlig, når et vilkårligt units-lag er til: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Synlig kun, mens imperial-laget er fra: /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;
Det understøttede subsæt er bevidst snævert. Policies er begrænset til ocmpAllOn og ocmpAnyOn; ocmpAnyOff og ocmpAllOff afvises. Udtryk er ét niveau dybe, med poceAll, poceAny eller poceNot over oprettede grupper, og /Not kræver præcis ét gruppeindeks. poceAll serialiseres som /And, poceAny som /Or og poceNot som /Not, de eneste tre operatorer, §8.11.2.2 definerer. Versioner før 3.181.2 skrev /All og /Any, som PDFium evaluerer som skjult, så har du oprettet OCMD'er med en ældre build, så regenerér dem frem for at stole på udtrykket. Hvert medlemskab behøver også mindst én indgang i MemberGroupIndices, selv når et udtryk er til stede, fordi listen bliver /OCs-arrayet. Objektnumrene på de nye dictionaries kommer tilbage i Report.CreatedMembershipObjectNumbers
To adfærdsformer er værd at kende, før du bygger videre på OCMD'er. For det første refererer de gatede forms i samme kald kun grupper: TPdfOptionalContentCreateForm.GroupIndex indekserer Groups, ikke Memberships, så en form kan ikke gater af en nyoprettet OCMD gennem denne API. For det andet tilføjer PDFium Component hver ny OCMD til katalogets /OCProperties /OCGs-array, mens /ON, /OFF og /Order forbliver gruppe-only. ISO 32000-1 §8.11.4.2 beskriver /OCGs som gruppelisten, men uden den registrering enumererer TPdf.InspectOptionalContent slet ikke medlemskabet. Bivirkningen er, at et inventar af outputtet lister hver OCMD som en ekstra unavngiven indgang i Inventory.Groups, så match grupper efter Name frem for efter position
Hvordan patcher komponenten en eksisterende /OCProperties?
TPdf.SaveAsOptionalContentCreated bygger enten /OCProperties fra bunden eller udvider den, der allerede er i kataloget, og den udvider ved at tilføje, aldrig ved at genopbygge. Et dokument uden lag får en ny dictionary med /OCGs og en default-konfiguration /D med /ON, /OFF og /Order plus /Locked og /RBGroups, når en gruppe beder om dem, som opstillet i ISO 32000-1 §8.11.4.3
For et dokument, der allerede har lag, patches hvert målarray ved at indsætte de nye referencer lige før dets afsluttende klamme. Det lyder omstændeligt, og det er der med vilje. Et /Order-array kan rumme indlejrede arrays og tekstetiketter, der former det lagtræ, en viewer viser, og en tidligere tilgang, der fladtede og genopbyggede arrayet, mistede de etiketter. Indsættelse til sidst holder det eksisterende træ intakt og lister simpelthen de nye grupper bagefter. Arrays, der bor i deres egne indirekte objekter, resolves og patches på stedet
Siden v3.175.0 kan default-konfigurationen selv også være indirekte. Nogle producenter skriver /OCProperties << /OCGs [...] /D 12 0 R >>, og PDFium Component følger nu den reference og patcher måldictionaryen frem for at fejle med en malformed-entry-fejl. Direkte og indirekte side-/Resources-dictionaries, inklusive en indirekte /XObject-subdictionary, håndteres på samme måde
At bevise, at de nye lag reelt toggler
Et lag, der eksisterer i /OCProperties men ikke gater noget, er let at producere og svært at opdage. Under udviklingen pakkede den første build af form-writeren et stream-objekt, der allerede bar sit eget N 0 obj-header og endobj-trailer, ind i en anden header. PDFium kunne ikke parse det fordoblede objekt og sprang i stedet for at rapportere en fejl stille Do-operatoren over. Filen åbnede, lagpanelet så rigtigt ud, og den gatede tegning optrådte aldrig. Det, der udstillede det, var en probe, der toggede laget og sammenlignede renderede pixels: ingen forskel, så noget var galt
Det er grunden til, at metoden verificerer, før den leverer. Outputtet genåbnes gennem en streng indlæsning, der tjekker cross-reference-tabellen, og hver anmodet gruppe læses tilbage gennem InspectOptionalContent for at bekræfte sit navn, sin starttilstand og sin låseregistrering. Først derefter kopieres outputtet til din stream, og fil-overloaden skriver en midlertidig fil og erstatter målet atomart. Du kan selv køre samme toggle-tjek ved at genåbne outputtet, finde gruppen efter navn og gemme en kopi med den modsatte tilstand
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 side ét af begge filer, og de blå og røde linjer bør skifte. Gør de ikke det, så inspicer gruppens EffectiveVisible-værdi i inventaret først; den afspejler den tilstand, PDFium reelt vil rendere, efter konfigurationsarrays og usage-regler er anvendt
Grænser og fælder at planlægge omkring
TPdf.SaveAsOptionalContentCreated er en afgrænset profil, og de fleste overraskelser kommer af at behandle den som en generel lageditor. Begrænsningerne nedenfor håndhæves på forhånd, så et krænkende kald fejler med Report.ErrorMessage sat og skriver intet output:
- Mindst én gruppe er påkrævet; grupper, medlemskaber og forms er lofter ved 4096 indgange hver, gruppenavne skal være unikke og 1 til 255 tegn lange, og hver form accepterer højst 16 MiB indholdsbytes
- Krypterede dokumenter afvises, og et signeret dokument behøver
AllowSignedDocument = True; outputtet er en fuldstændig omskrivning, så eksisterende signaturer validerer ikke længere - Hver
Forms-indgang bliver sit eget Form XObject, så at lægge samme overlay på 200 sider gemmer indholdet 200 gange frem for én - Den nye
Do-stream tilføjes efter det eksisterende sideindhold, så den kører i den graphics state, de tidligere streams efterlader; velformede sider gendanner deres tilstand, men en side med ubalanceredeq/Qkan flytte dit overlay - Eksisterende sideindhold flyttes aldrig ind i et lag; kun tegningen, du giver med i
Forms, gater
Repetitionsarket er det, der skal vejes for store dokumenter. Er overlayet tungt og gentager sig på hver side, holder tilgangen med delte objekter i genbrugelige sidestempler bygget på Form XObjects én kopi i filen, på bekostning af selv at håndtere lagporten
FAQ
Kan PDFium Component lægge eksisterende sideindhold ind i et nyt lag?
Nej. TPdf.SaveAsOptionalContentCreated gater kun det indhold, du leverer i Forms; den pakker ikke eksisterende operatorer ind i marked content eller flytter dem ind i et XObject. For at laglægge eksisterende grafik, regenerér den som selvstændigt formindhold og fjern den oprindelige tegning i et separat trin
Hvorfor er mine gatede annotationer stadig synlige, når laget er fra?
PDFium tjekker ikke /OC-indgangen af annotationsdictionaries på sin renderti, så annotationsniveau-gating har ingen effekt i PDFium-baserede viewers. Det er grunden til, at PDFium Component knytter lagindhold gennem Form XObjects, som PDFium reelt gater
Hvilke objektnumre skal jeg bruge, når jeg senere skifter de nye lag?
Report.CreatedGroupObjectNumbers holder numrene i den fil, der lige blev skrevet. Når du genåbner den fil og konfigurerer den, så tag ObjectNumber fra TPdf.InspectOptionalContent og match efter Name, som forbliver korrekt, selv hvis en senere gemning renummererer objekter
PDFium Component udsender SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured og InspectOptionalContent som ét optional content-værktøjssæt til Delphi og C++Builder, bygget på PDFium-rendereren. Detaljer og downloads findes på PDFium Component-produktsiden