PDFium Component crea nuovi layer PDF da Delphi attraverso TPdf.SaveAsOptionalContentCreated. In un solo salvataggio verificato il metodo aggiunge gruppi di contenuto opzionale (OCG), dizionari di membership del contenuto opzionale (OCMD) e disegni forniti dal chiamante avvolti in Form XObject la cui voce /OC punta a un nuovo gruppo, così PDFium e altri viewer mostrano o nascondono quel disegno quando il layer viene commutato
La richiesta raramente è astratta. Un team di impiantistica vuole quote metriche e imperiali sullo stesso foglio di disegno, con esattamente un insieme visibile. Una tipografia vuole un overlay delle linee di taglio che si vede a schermo ma si può spegnere prima della prova. Nessuno dei due file ha ancora layer, quindi non c'è nulla da commutare. Commutare layer esistenti è un lavoro diverso, trattato in commutare i layer di contenuto opzionale PDF esistenti con SaveAsOptionalContentConfigured; questo articolo parla di creare i layer in primo luogo
Perché non si può semplicemente impostare /OC su un page object con PDFium?
PDFium non ha una funzione pubblica per attaccare contenuto opzionale a un page object esistente. Gli header pubblici non contengono FPDFPageObj_SetOC né alcunché di equivalente, quindi la solita strada edit-poi-FPDFPage_GenerateContent non può produrre contenuto gated. Il gate va scritto nella struttura del file, e l'unica domanda è quale struttura PDFium onorerà quando renderizza
ISO 32000-1 §8.11.3 dà tre modi per rendere la grafica opzionale, e PDFium li tratta diversamente:
- Sezioni del content stream di pagina avvolte in marked content
/OC /Name BDC...EMC, controllate per page object durante il rendering - Form XObject e image XObject che portano una voce
/OCnel proprio dizionario, controllati quando l'XObject viene dipinto - Annotazioni con una voce
/OCnel dizionario di annotazione, che il percorso di rendering di PDFium non consulta affatto, così un'annotazione gated resta visibile a prescindere dallo stato del layer
L'avvolgimento in marked content significa riscrivere i content stream esistenti operatore per operatore, e le annotazioni sono escluse dal renderer. Resta la strada del Form XObject, ed è quella che usa PDFium Component: il vostro disegno va in un nuovo Form XObject con /OC che punta al gruppo, la form viene registrata nel dizionario /Resources /XObject della pagina, e un nuovo content stream che contiene un singolo /Name Do viene accodato al /Contents della pagina. Il contenuto esistente della pagina non viene mai analizzato né riscritto
Descrivere gruppi e form gated in Delphi
TPdf.SaveAsOptionalContentCreated prende tre array e restituisce un report. Entrambi gli overload, uno che scrive su un TStream e uno su un nome di file, condividono la stessa lista di parametri: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport, e un AllowSignedDocument: Boolean = False facoltativo. Il metodo è arrivato in PDFium Component v3.154.0
Ogni TPdfOptionalContentCreateGroup ha un Name, un Creator facoltativo, un Intent pari a pociView o pociDesign, un flag InitialStateVisible, un flag Locked e un RadioTag. I gruppi che condividono lo stesso RadioTag finiscono in un unico array /RBGroups, che è come un viewer sa che accenderne uno spegne gli altri. Ogni TPdfOptionalContentCreateForm nomina il proprio gruppo di gating con GroupIndex (un indice in base zero nel vostro array Groups), un PageNumber in base 1, i byte grezzi del content stream, un bounding box, una matrice facoltativa e un ResourceName facoltativo
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 azzera i record
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;
Notate che il contenuto della form è disegno vettoriale puro. Il componente scrive solo un dizionario /Resources vuoto su ogni form, quindi il content stream deve essere autosufficiente: colori device, tracciati, ritagli e transform funzionano, ma qualunque cosa che richieda una risorsa con nome (un font per Tf, un'immagine per Do, un ExtGState per gs) non ha nulla contro cui risolvere. Se lasciate ResourceName vuoto, le form si chiamano FmOc1, FmOc2 e così via; un nome personalizzato può contenere solo lettere, cifre, underscore, meno e punto
Cosa aggiunge un OCMD rispetto a un gruppo semplice?
Un dizionario di membership del contenuto opzionale fa dipendere la visibilità da parecchi gruppi insieme. ISO 32000-1 §8.11.2.2 definisce due forme: un elenco /OCs con una policy /P come /AnyOn o /AllOn, e un'espressione di visibilità /VE costruita con /And, /Or e /Not nella specifica. PDFium Component prende un TPdfOptionalContentCreateMembership con MemberGroupIndices, una Policy e una Expression; quando Expression.GroupIndices non è vuoto vince l'espressione e /VE viene scritta accanto a /OCs
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Visibile quando uno qualunque dei layer di unità è acceso: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Visibile solo finché il layer imperiale è spento: /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;
Il sottoinsieme supportato è volutamente ristretto. Le policy sono limitate a ocmpAllOn e ocmpAnyOn; ocmpAnyOff e ocmpAllOff vengono rifiutate. Le espressioni sono profonde un livello, con poceAll, poceAny o poceNot sui gruppi creati, e /Not richiede esattamente un indice di gruppo. poceAll si serializza come /And, poceAny come /Or e poceNot come /Not, gli unici tre operatori che §8.11.2.2 definisce. Le versioni prima della 3.181.2 scrivevano /All e /Any, che PDFium valuta come nascosti, quindi se avete creato OCMD con una build più vecchia, rigenerateli invece di fidarvi dell'espressione. Ogni membership richiede anche almeno una voce in MemberGroupIndices, anche quando un'espressione è presente, perché quell'elenco diventa l'array /OCs. I numeri di oggetto dei nuovi dizionari tornano in Report.CreatedMembershipObjectNumbers
Due comportamenti vale la pena conoscere prima di costruire sugli OCMD. Primo, le form gated nella stessa chiamata referenziano solo gruppi: TPdfOptionalContentCreateForm.GroupIndex indicizza Groups, non Memberships, quindi una form non può essere murata da un OCMD appena creato attraverso questa API. Secondo, PDFium Component aggiunge ogni nuovo OCMD all'array /OCProperties /OCGs del catalogo, mentre /ON, /OFF e /Order restano solo per i gruppi. ISO 32000-1 §8.11.4.2 descrive /OCGs come l'elenco dei gruppi, ma senza quella registrazione TPdf.InspectOptionalContent non enumera affatto la membership. L'effetto collaterale è che un inventario dell'output elenca ogni OCMD come voce extra senza nome in Inventory.Groups, quindi abbinate i gruppi per Name invece che per posizione
Come mette in patch il componente un /OCProperties esistente?
TPdf.SaveAsOptionalContentCreated oppure costruisce /OCProperties da zero oppure estende quello già presente nel catalogo, ed estende accodando, mai ricostruendo. Un documento senza layer riceve un nuovo dizionario con /OCGs e una configurazione predefinita /D che porta /ON, /OFF e /Order, più /Locked e /RBGroups quando qualche gruppo le richiede, come disposto in ISO 32000-1 §8.11.4.3
Per un documento che ha già layer, ogni array di destinazione viene messo in patch inserendo i nuovi riferimenti appena prima della sua parentesi quadra di chiusura. Sembra un dettaglio minuzioso, e c'è per un motivo. Un array /Order può contenere array annidati ed etichette testuali che danno forma all'albero dei layer che un viewer mostra, e un approccio precedente che appiattiva e ricostruiva l'array perdeva quelle etichette. L'inserimento in coda tiene intatto l'albero esistente e semplicemente elenca i nuovi gruppi dopo. Gli array che vivono nei propri oggetti indiretti vengono risolti e messi in patch sul posto
Dalla v3.175.0 anche la configurazione predefinita stessa può essere indiretta. Alcuni produttori scrivono /OCProperties << /OCGs [...] /D 12 0 R >>, e PDFium Component ora segue quel riferimento e mette in patch il dizionario di destinazione invece di fallire con un errore di voce malformata. I dizionari /Resources di pagina diretti e indiretti, compreso un sottodizionario /XObject indiretto, vengono trattati allo stesso modo
Dimostrare che i nuovi layer commutano davvero
Un layer che esiste in /OCProperties ma non mura nulla è facile da produrre e difficile da notare. Durante lo sviluppo la prima build del writer delle form avvolse un oggetto stream che portava già il proprio header N 0 obj e il trailer endobj dentro un secondo header. PDFium non riusciva a fare il parsing dell'oggetto raddoppiato, e invece di riportare un errore saltava silenziosamente l'operatore Do. Il file si apriva, il pannello dei layer sembrava giusto, e il disegno murato non compariva mai. A esporlo è stata una sonda che commutava il layer e confrontava i pixel renderizzati: nessuna differenza, quindi qualcosa non andava
Ecco perché il metodo verifica prima di consegnare. L'output viene riaperto attraverso un caricamento rigoroso che controlla la tabella dei riferimenti incrociati, e ogni gruppo richiesto viene riletto attraverso InspectOptionalContent per confermare il proprio nome, il proprio stato iniziale e la propria registrazione locked. Solo allora l'output viene copiato nel vostro stream, e l'overload su file scrive un file temporaneo e sostituisce la destinazione atomicamente. Potete eseguire voi lo stesso controllo di commutazione riaprendo l'output, trovando il gruppo per nome e salvando una copia con lo stato opposto
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;
Renderizzate la pagina uno di entrambi i file e le linee blu e rossa dovrebbero scambiarsi. Se non lo fanno, ispezionate prima il valore EffectiveVisible del gruppo nell'inventario; riflette lo stato che PDFium renderizzerà davvero dopo che array di configurazione e regole d'uso sono applicati
Limiti e trappole di cui tenere conto
TPdf.SaveAsOptionalContentCreated è un profilo delimitato, e la maggior parte delle sorprese viene dal trattarlo come un editor di layer generale. I vincoli sotto sono imposti all'inizio, così una chiamata che li viola fallisce con Report.ErrorMessage impostato e non scrive alcun output:
- È richiesto almeno un gruppo; gruppi, membership e form hanno un tetto di 4096 voci ciascuno, i nomi dei gruppi devono essere unici e lunghi da 1 a 255 caratteri, e ogni form accetta al massimo 16 MiB di byte di contenuto
- I documenti cifrati vengono rifiutati, e un documento firmato richiede
AllowSignedDocument = True; l'output è una riscrittura completa, quindi le firme esistenti non verificheranno più - Ogni voce
Formsdiventa il proprio Form XObject, quindi mettere lo stesso overlay su 200 pagine memorizza il contenuto 200 volte invece che una - Il nuovo stream
Doviene accodato dopo il contenuto esistente della pagina, quindi gira nello stato grafico che gli stream precedenti lasciano dietro; le pagine ben formate ripristinano il proprio stato, ma una pagina conq/Qsbilanciati può spostare il vostro overlay - Il contenuto esistente della pagina non viene mai spostato in un layer; solo il disegno che passate in
Formsviene murato
Il punto della ripetizione è quello da valutare per i documenti grandi. Se l'overlay è pesante e si ripete su ogni pagina, l'approccio a oggetti condivisi in timbri di pagina riutilizzabili costruiti su Form XObject tiene una sola copia nel file, al costo di gestire da voi il gate del layer
FAQ
PDFium Component può mettere il contenuto esistente della pagina in un nuovo layer?
No. TPdf.SaveAsOptionalContentCreated mura solo il contenuto che fornite in Forms; non avvolge gli operatori esistenti in marked content né li sposta in un XObject. Per mettere su layer una grafica esistente, rigeneratela come contenuto di form autosufficiente e togliete il disegno originale in un passo separato
Perché le mie annotazioni gated restano visibili quando il layer è spento?
PDFium non controlla la voce /OC dei dizionari di annotazione sul proprio percorso di rendering, quindi il gating a livello di annotazione non ha effetto nei viewer basati su PDFium. È il motivo per cui PDFium Component attacca il contenuto dei layer attraverso Form XObject, che PDFium mura davvero
Quali numeri di oggetto usare quando in seguito commuto i nuovi layer?
Report.CreatedGroupObjectNumbers contiene i numeri nel file appena scritto. Quando riaprite quel file e lo configurate, prendete ObjectNumber da TPdf.InspectOptionalContent e abinate per Name, che resta corretto anche se un salvataggio successivo rinumera gli oggetti
PDFium Component include SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured e InspectOptionalContent come un unico toolkit di contenuto opzionale per Delphi e C++Builder, costruito sul renderer PDFium. Dettagli e download sono sulla pagina del prodotto PDFium Component