Un corredo di tavole arriva con le quote su un livello tutto loro, e il modo ovvio di nasconderle è spostare il riferimento di quel gruppo dall'array /ON della configurazione predefinita in /OFF. Il file si salva e si riapre, e le linee di quota sono ancora stampate sopra il cartiglio. La causa sta a un oggetto di distanza, dentro il dizionario del gruppo: /Usage << /View << /ViewState /ON >> >>, uno stato esplicito che il renderer legge prima degli array di configurazione
Lo stato di un livello può vivere in più posti di un PDF, e una modifica a /OCProperties ne tocca solo uno. Il motore PDFium di Google renderizza l'optional content correttamente, ma la sua API C pubblica non ha nessuna funzione per enumerare i gruppi di optional content (OCG), scegliere una configurazione o accendere e spegnere un gruppo. Il codice Delphi che comanda i livelli deve riscrivere gli oggetti da sé e riprodurre esattamente l'ordine decisionale del renderer, o la sua lista di livelli riporta uno stato mentre la pagina ne mostra un altro
Perché modificare /ON e /OFF non cambia ciò che PDFium renderizza?
Gli array /ON e /OFF sono solo il secondo di cinque passi, e due passi successivi possono scavalcarli. La ISO 32000-1 §8.11.4.3 definisce il dizionario di configurazione optional content con /BaseState, /ON, /OFF, /Locked, /RBGroups e /AS, mentre la §8.11.4.4 definisce il dizionario Usage che un OCG può portare. Nella specifica, Usage descrive a cosa serve un gruppo, e le voci auto-state in /AS trasformano quella descrizione in ON o OFF. PDFium prende una strada più corta: quando renderizza per uno scopo, vince lo stato esplicito del gruppo per quello scopo. Fuse insieme, l'ordine appare così:
- Partite dal
/BaseStatedella configurazione, dove/ONe/Unchangedcontano entrambi come visibili e solo/OFFnasconde - Applicate l'array
/ON, poi l'array/OFF, così un gruppo elencato in entrambi finisce nascosto - Applicate lo stato Usage esplicito del gruppo per lo scopo richiesto, come
/Usage << /View << /ViewState /OFF >> >>, che scavalca tutto ciò che sta sopra - Trattate come visibile un gruppo il cui
/Intentnon contiene né/Viewné/All, visto che non prende parte alla visibilità per intent di vista - Eseguite per ultimo l'array
/ASdella configurazione selezionata, lasciando che le voci per l'evento corrispondente impostino lo stato dei gruppi che elencano
Il terzo passo spiega il file di apertura: alcuni exporter scrivono /ViewState /ON in ogni gruppo che creano, e da quel momento nessuna quantità di editing degli array nasconde quei livelli. Stampa ed export seguono la stessa regola con una variante: il renderer legge prima /PrintState o /ExportState e ripiega su /ViewState quando la voce specifica manca. Un solitario /ViewState /ON inchioda quindi il livello anche sulla carta. Il quarto passo è più silenzioso. Un gruppo taggato /Intent /Design sta fuori dalla visibilità per intent di vista e dipinge a prescindere dagli array, mentre un gruppo senza chiave /Intent viene trattato come /View, che è il default che la specifica dà
Elencare i livelli e il loro stato renderizzato con InspectOptionalContent
PDFium Component riproduce quell'ordine in TPdf.InspectOptionalContent. Il metodo riempie un TPdfOptionalContentInventory e restituisce True in caso di successo; il suo parametro Usage vale ocuView per default e il suo parametro ConfigurationIndex vale -1, che seleziona l'indice 0. L'indice di configurazione 0 è il dizionario predefinito /D, e le voci di /Configs seguono dall'indice 1 in ordine di file. Prima della scansione, il metodo fa salvare a PDFium il documento aperto in memoria ed espande gli object stream, così le modifiche fatte prima nella sessione compaiono nel risultato. Un file senza /OCProperties, o un indice di configurazione fuori dall'intervallo disponibile, fa restituire False alla chiamata con il motivo in ErrorMessage; nessuna eccezione viene sollevata per nessuno dei due casi
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
Cfg: TPdfOptionalContentConfiguration;
G: TPdfOptionalContentGroup;
begin
// Usage vale ocuView per default; -1 seleziona la configurazione 0, il dizionario /D
if not Pdf.InspectOptionalContent(Inv) then
begin
Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
Exit;
end;
Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
[Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
BoolToStr(Cfg.HasAutomaticState, True)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %-24s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Ogni TPdfOptionalContentGroup porta il proprio numero di oggetto, il nome, gli Intents e i tre stati espliciti ViewState, PrintState e ExportState, ciascuno tra ocsUnspecified, ocsOn o ocsOff. Quando uno stato legge ocsOn e il gruppo sta nell'array /OFF, avete trovato la trappola del paragrafo di apertura. Locked e RadioGroupIndex arrivano dalla configurazione selezionata, e RadioGroupIndex vale -1 per un gruppo che non appartiene a nessun insieme /RBGroups
Il resto del dizionario Usage viene riportato così come lo dichiara il file: Language e LanguagePreferred, UserType e UserNames, PageElementSubtype, e MinimumZoom e MaximumZoom dietro un flag HasZoomRange. EffectiveVisible non dipende da nessuno di questi. La visualizzazione dipendente dallo zoom è roba del vostro viewer. Ogni TPdfOptionalContentConfiguration espone nome, BaseState, contenuti degli array e RadioGroups, più OrderGroupObjectNumbers, che appiattisce /Order in riferimenti a gruppi e salta le etichette testuali degli array di etichette annidati
Stampa ed export possono essere in disaccordo con lo schermo
L'argomento Usage cambia la risposta, ed è il punto per cui esiste. Sotto ocuPrint l'inventario legge /PrintState con il suo ripiego su /ViewState, e prendono parte solo le voci /AS il cui /Event è /Print. Un livello filigrana costruito per comparire solo sulla carta quindi si riporta nascosto sotto ocuView e visibile sotto ocuPrint. Confrontare i due passaggi trova questi livelli prima che una stampa sorprenda qualcuno
procedure TFormMain.ReportPrintOnlyLayers;
var
OnScreen, OnPaper: TPdfOptionalContentInventory;
I: Integer;
begin
if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
Exit;
// Entrambe le passate leggono lo stesso array /OCGs, così l'ordine dei gruppi combacia
for I := 0 to High(OnScreen.Groups) do
if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
[string(OnScreen.Groups[I].Name),
BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;
Come vengono valutate le espressioni di visibilità OCMD?
Un dizionario di appartenenza optional content (OCMD, ISO 32000-1 §8.11.2.2) lega un pezzo di contenuto a più gruppi tramite una policy /P, o tramite un'espressione di visibilità /VE che combina gruppi con /And, /Or e /Not. La specifica dà a /P un default di /AnyOn, permette a /VE di avere la precedenza su /OCGs e /P quando presente, e dice che un OCMD senza gruppi non ha effetto sulla visibilità
L'array Memberships dell'inventario segue quelle regole contro i risultati per gruppo della stessa passata, così l'usage che avete chiesto si porta dietro. Ogni appartenenza riporta i numeri di oggetto dei propri gruppi, la Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, o ocmpUnknown per un nome fuori da quei quattro), il testo grezzo di VisibilityExpression e il proprio EffectiveVisible. Un OCMD senza gruppi conta come visibile. Anche un riferimento a un numero di oggetto che non è un OCG noto conta come visibile, così un riferimento appeso non nasconde tutto ciò che gli sta intorno. La valutazione di /VE si ferma a una profondità di annidamento di 32 e tratta tutto ciò che sta più in profondità come nascosto, così un'espressione autoriferita non può far traboccare lo stack. La scansione elenca ogni OCMD conservato come oggetto indiretto con /Type /OCMD, object stream inclusi
Scrivere un nuovo stato di livello con SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured prende un array di record TPdfOptionalContentStateChange, ognuno con un GroupObjectNumber e un flag Visible. Scrive una copia in cui la configurazione selezionata produce esattamente quello stato sotto l'ordine in cinque passi. I gruppi che non menzionate conservano la visibilità che l'inventario ha calcolato per loro
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
ForPrint: Boolean);
var
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
SetLength(Changes, 2);
Changes[0].GroupObjectNumber := DimensionsObj;
Changes[0].Visible := False;
Changes[1].GroupObjectNumber := NotesObj;
Changes[1].Visible := True;
Options := TPdfOptionalContentConfigureOptions.Default;
if ForPrint then
begin
Options.Usage := ocuPrint; // scrive /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // prima voce di /Configs, /D resta com'era
end;
if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
Changes, Options, Report) then
raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);
Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
[Report.ChangedGroupCount, Report.GroupCount,
Report.UpdatedUsageStateCount,
BoolToStr(Report.RemovedAutomaticState, True)]));
end;
L'output conserva il documento salvato da PDFium stesso come prefisso byte per byte e accoda solo il proprietario di configurazione riscritto e gli oggetti OCG che portano dizionari Usage, seguiti da una nuova sezione xref e dal trailer. Prima che qualcosa raggiunga la destinazione, il risultato viene riaperto in un TPdf separato sotto la policy di caricamento rigorosa, e il metodo fallisce se quel caricamento non riesce o la tabella dei riferimenti incrociati non valida. L'overload su file scrive in un file temporaneo accanto alla destinazione e sostituisce la destinazione solo dopo la verifica, così un aggiornamento respinto lascia la tavola precedente intoccata. È lo stesso approccio a revisione incrementale verificato usato dal editor di name tree e number tree PDF in PDFium Component. Per confermare un preset di stampa, ricaricate l'output e chiamate InspectOptionalContent(Inv, ocuPrint, 1); i gruppi dovrebbero riportare gli stati che avete passato
Cosa si rifiuta di fare il salvataggio configurato?
Il salvataggio configurato rifiuta qualsiasi modifica che il documento vieta o non può trasportare in sicurezza, e ogni rifiuto avviene prima che la destinazione venga scritta. I controlli girano in un ordine fisso. Un documento firmato solleva EPdfError prima di qualsiasi altra cosa a meno che passiate AllowSignedDocument = True. Un documento cifrato viene respinto subito dopo, perché gli oggetti incrementali in chiaro non possono portare l'handler di sicurezza attivo. Poi ogni modifica richiesta viene controllata: un numero di oggetto che non sta in /OCGs fallisce a prescindere, e una modifica a un gruppo elencato nell'array /Locked della configurazione fallisce, benché ribadire il suo valore corrente passi. Per ultimo, con EnforceRadioGroups attivo, un insieme /RBGroups che finirebbe con più di un membro visibile viene respinto. Ogni rifiuto tranne quello della firma restituisce False con il motivo in Report.ErrorMessage
Due di quei cancelli vanno oltre ciò che la specifica richiede strettamente. La §8.11.4.3 descrive /Locked come protezione contro le modifiche attraverso l'interfaccia utente del viewer e descrive i gruppi radio button come lo spegnimento degli altri membri quando uno si accende. PDFium Component applica entrambi anche alle modifiche programmatiche, e per i gruppi radio rifiuta invece di scegliere membri da spegnere. Chiesti due membri di un insieme mutuamente esclusivo, la libreria non può sapere quale intendevate, e un'ipotesi contraddirebbe la vostra richiesta. Spegnete gli altri nello stesso array Changes e il salvataggio passa
Il cancello dei documenti firmati esiste perché la revisione accodata sta sopra il re-save completo di PDFium, che è una nuova sequenza di byte; il byte range coperto da una firma esistente non può darsi per sopravvissuto. Passate AllowSignedDocument = True solo quando perdere quella firma è accettabile
Tre effetti collaterali contano in un job in batch. La riscrittura normalizza la configurazione selezionata a /BaseState /ON, così una baseline /Unchanged o /OFF di un autore diventa array espliciti con la stessa visibilità risultante. Eliminare /AS toglie anche i trucchi solo-carta come un livello filigrana che compare solo in stampa; impostate DisableAutomaticState a False per conservare quelle regole e accettare che possano scavalcare il vostro stato richiesto per il loro evento. E gli array /ON e /OFF vengono scritti dagli stati calcolati per Options.Usage, quindi un preset di stampa salvato nella configurazione 0 cambia anche ciò che lo schermo mostra per ogni gruppo privo di dizionario Usage. Tenete i preset di stampa in una voce di /Configs e lasciate /D allo schermo. Sul versante conformità, PDF/A-2 (ISO 19005-2 clausola 6.9) e PDF/UA (ISO 14289-1 clausola 7.10) vietano entrambi /AS nei dizionari di configurazione, così l'output predefinito spegne un riscontro che la vostra validazione preflight PDF/A con PDFium Component segnalerebbe altrimenti
Applicare le modifiche ai livelli dentro un viewer Delphi
In un viewer, il controllo dei livelli è una checklist costruita dall'inventario, seguita da un salvataggio in memoria e da un ricaricamento. Riempite la checklist da Groups, disabilitate le voci con flag Locked impostato, trattate i membri che condividono uno RadioGroupIndex come mutuamente esclusivi, e all'applicazione scrivete su un TMemoryStream attraverso l'overload stream. TPdf.LoadDocument si aspetta un componente inattivo, quindi spegnetelo prima; il caricamento bufferizzato copia i byte, il che vi lascia liberare lo stream subito
procedure TFormMain.ApplyLayerChecklist(
const Changes: TPdfOptionalContentStateChanges);
var
Report: TPdfOptionalContentConfigureReport;
Output: TMemoryStream;
KeepPage: Integer;
begin
Output := TMemoryStream.Create;
try
try
if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
TPdfOptionalContentConfigureOptions.Default, Report) then
begin
ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
Exit;
end;
except
on E: EPdfError do
begin
ShowMessage(E.Message); // file firmato: Output è ancora vuoto
Exit;
end;
end;
KeepPage := Pdf.PageNumber;
Pdf.Active := False;
Pdf.LoadDocument(Output); // Buffered vale True per default
Pdf.PageNumber := KeepPage;
finally
Output.Free;
end;
end;
Dopo il ricaricamento, PDFium dipinge dalla nuova configurazione, così lo schermo e la successiva chiamata a InspectOptionalContent concordano. Il cablaggio tra TPdf e TPdfView è trattato in costruire un viewer PDF ricco di funzionalità con PDFium VCL in Delphi. Quando un documento non ha ancora livelli e dovete aggiungerli, creare livelli PDF con OCG e OCMD in Delphi passa attraverso SaveAsOptionalContentCreated
Prezzi, la build di prova e le altre funzionalità documentali del componente sono elencate sulla pagina prodotto PDFium Component for Delphi