Articolo tecnico

Gestire i livelli Optional Content PDF in Delphi con PDFium

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 /BaseState della configurazione, dove /ON e /Unchanged contano entrambi come visibili e solo /OFF nasconde
  • 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 /Intent non contiene né /View né /All, visto che non prende parte alla visibilità per intent di vista
  • Eseguite per ultimo l'array /AS della configurazione selezionata, lasciando che le voci per l'evento corrispondente impostino lo stato dei gruppi che elencano
La decisione di visibilità in cinque passi che PDFium Component ripete per ogni gruppo optional content PDF in Delphi: BaseState fissa il punto di partenza, gli array ON e OFF della configurazione si applicano in ordine, una voce Usage esplicita di ViewState o PrintState scavalca entrambi, la non partecipazione per Intent conta come visibile, e l'array AS gira per ultimo
Modificare gli array ON e OFF non basta perché PDFium fonde BaseState, entrambi gli array, lo stato Usage del gruppo e infine le regole auto-state AS in un solo verdetto che InspectOptionalContent riproduce passo per passo

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

Come SaveAsOptionalContentConfigured in PDFium Component scrive un PDF Delphi con i livelli commutati: le modifiche di stato e le opzioni entrano, la configurazione selezionata viene riscritta con array ON e OFF completi e stati Usage, la revisione incrementale verificata viene accodata, e una riapertura severa deve validare prima che qualsiasi cosa venga scritta
Il salvataggio configurato conserva il re-save di PDFium stesso come prefisso di byte, accoda il proprietario di configurazione riscritto più una nuova sezione xref, e riapre il risultato in un TPdf separato prima che la destinazione venga toccata

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

I cancelli di rifiuto che SaveAsOptionalContentConfigured applica in PDFium Component prima di scrivere un PDF Delphi configurato: un numero di oggetto fuori dagli OCG fallisce, i gruppi bloccati falliscono, gli insiemi RBGroups con più di un membro visibile vengono respinti, i documenti cifrati non possono trasportare oggetti incrementali in chiaro, e i file firmati esigono AllowSignedDocument
Ogni rifiuto avviene prima che la destinazione venga toccata; la ragione atterra in Report.ErrorMessage, o nel messaggio di EPdfError quando il file è firmato

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