Articolo tecnico

Livelli PDF in Delphi: Optional Content Groups (OCG)

Un geometra apre una planimetria e vuole nascondere le curve di livello mantenendo visibili i sottoservizi. Un revisore desidera che le annotazioni in rosso siano visibili sullo schermo ma assenti dalla stampa. Una scheda prodotto viene distribuita in tre lingue all'interno dello stesso file, e il lettore sceglie quale lingua visualizzare. In tutti e tre i casi si tratta della stessa funzionalità PDF, e il pannello che la gestisce in Acrobat si chiama Livelli. La tecnologia alla base di questo pannello è il contenuto opzionale (optional content), ed è ciò che consente a una singola pagina di contenere diversi strati visivi indipendenti che un visualizzatore può attivare o disattivare

Il contenuto opzionale è specificato nello standard ISO 32000-1 §8.11. L'unità di visibilità è un optional content group, ovvero un OCG, un dizionario di tipo /OCG con un nome. Il contenuto marcato su una pagina è associato a un gruppo, e il visualizzatore decide se tale gruppo debba essere mostrato. Un costrutto correlato, il dizionario di appartenenza a contenuto opzionale o OCMD, consente di far dipendere la visibilità da una combinazione booleana di più gruppi, ma il caso comune prevede un singolo gruppo con nome corrispondente a un singolo livello. Il documento unisce l'intero meccanismo attraverso una voce del catalogo, /OCProperties, descritta di seguito

Cosa deve contenere il catalogo

Un OCG da solo è inerte. Affinché un visualizzatore possa elencare un livello e memorizzarne lo stato, il catalogo del documento necessita di un dizionario /OCProperties, e il §8.11.4 specifica esattamente cosa deve contenere. C'è un array /OCGs che elenca ogni gruppo presente nel file, e c'è una voce /D che contiene la configurazione predefinita. La configurazione predefinita è la parte che un lettore applica al momento dell'apertura del file. Registra quali gruppi iniziano come attivi e quali come disattivi, quali voci sono bloccate per impedire all'utente di modificarle e, attraverso un array /Order, come i nomi dei livelli sono disposti e nidificati nel pannello

Struttura dei gruppi di contenuto opzionale PDF dietro PDF Library for Delphi: la voce OCProperties del catalogo elenca ogni livello OCG e la sua configurazione predefinita con stati iniziali, blocchi e ordine del pannello, mentre il contenuto marcato di pagina lega i disegni ai gruppi
I layer diventano indirizzabili solo attraverso il catalogo: OCProperties elenca ogni OCG e la configurazione predefinita registra stati iniziali, blocchi e ordine del pannello

La conseguenza pratica è che la creazione di un livello non è mai un'azione puramente locale. Il gruppo deve essere disegnato sulla pagina e deve anche essere registrato in una struttura a livello di catalogo che prima non esisteva. PDF Library for Delphi fa entrambe le cose al posto tuo. La prima chiamata che crea un gruppo aggiunge la voce /OCProperties al catalogo e inizializza la configurazione predefinita, in modo che il livello venga sia disegnato sia elencato senza richiedere un tracciamento separato da parte tua

Perché una modalità di conformità può inibire la funzionalità

Prima che venga eseguito qualsiasi codice relativo ai livelli, l'obiettivo di conformità del documento decide se il contenuto opzionale sia ammesso. Il formato PDF/A-1, il profilo di archiviazione definito in ISO 19005-1, vieta del tutto la voce /OCProperties nel §6.1.13. Il motivo risiede nello scopo del formato. Un file di archivio deve essere visualizzato in modo identico da ogni lettore nel futuro a lungo termine, e un contenuto la cui visibilità può essere modificata da un visualizzatore è un contenuto il cui aspetto non è fisso; pertanto, il profilo vieta il costrutto piuttosto che consentire un archivio ambiguo. I formati PDF/A-2 and PDF/A-3, definiti in ISO 19005-2 e ISO 19005-3, adottano la visione opposta nel §6.9 e consentono il contenuto opzionale, con specifiche regole sulla visibilità predefinita

Questa differenza si riflette direttamente nell'API. Quando il documento è in modalità PDF/A-1, NewOptionalContentGroup rifiuta di creare il gruppo e restituisce zero, poiché soddisfare la richiesta produrrebbe un file non conforme allo standard dichiarato. In modalità PDF/A-2 o PDF/A-3, e nei PDF ordinari senza vincoli, la stessa chiamata va a buon fine e restituisce un ID gruppo diverso da zero. Un risultato pari a zero non indica quindi un errore generico da analizzare in seguito; è la libreria a comunicare che il livello di conformità attivo non supporta questa funzionalità

var
  Pdf: TPDFlib;
  LayerID: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);                       // PDF/A-1a: OCProperties forbidden

    LayerID := Pdf.NewOptionalContentGroup('Utilities');
    if LayerID = 0 then
      // rifiutato sotto PDF/A-1; non è un errore transitorio, la modalità vieta i layer
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

Due stati per livello, non uno

Un livello non è semplicemente visibile o invisibile. La configurazione predefinita registra il suo stato sullo schermo e uno stato di stampa separato, poiché il §8.11.4 distingue ciò che un visualizzatore mostra da ciò che un processo di stampa emette. I due aspetti sono volutamente indipendenti. Una filigrana bozza può essere mostrata a schermo ma esclusa dalla carta, e un livello di linee di taglio può essere nascosto sullo schermo ma inviato a un plotter. Unire i due stati costringerebbe l'uno a seguire l'altro, perdendo il controllo che questa funzione è nata per offrire

PDF Library for Delphi espone la coppia tramite due metodi di impostazione. SetOptionalContentGroupVisible accetta l'ID del gruppo e un flag (in cui uno significa visibile e zero nascosto) e regola lo stato predefinito a schermo. SetOptionalContentGroupPrintable accetta l'ID del gruppo e un flag che indica se il livello viene emesso durante la stampa del documento. I corrispondenti metodi di lettura, GetOptionalContentGroupVisible e GetOptionalContentGroupPrintable, restituiscono uno o zero, permettendoti di leggere separatamente la disposizione a schermo e di stampa di un livello invece di dedurre l'una dall'altra

Matrice degli stati di visibilità indipendenti schermo e stampa per i livelli PDF creati con la PDF Library Delphi: i livelli di utilità restano visibili ovunque, la marcatura del revisore si mostra a schermo ma non stampa mai, e una linea di taglio per plotter si nasconde a schermo eppure stampa comunque
Visibilità a schermo ed emissione in stampa si impostano per gruppo, così i layer di servizio possono apparire ovunque mentre il markup del revisore resta una nota solo a schermo

Creare due livelli su una pagina

La creazione di un livello e il suo popolamento seguono un ordine preciso. Si disegna il contenuto del livello sulla pagina corrente, quindi si chiama SetContentStreamOptional passando l'ID del gruppo, operazione che racchiude il flusso di contenuto corrente della pagina in modo che tutto ciò che è stato disegnato finora appartenga a quel gruppo. Poiché la chiamata cattura tutto ciò che si trova sul flusso in quel preciso momento, la prassi consiste nel tracciare gli elementi di un livello, assegnarli e solo allora iniziare il livello successivo. L'esempio seguente posiziona i sottoservizi sulla prima pagina e le annotazioni di revisione su una seconda pagina, imposta lo stato a schermo e di stampa di ciascun livello e salva il documento

var
  Pdf: TPDFlib;
  FontID, UtilLayer, RedlineLayer: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;                          // unconstrained PDF: layers allowed
    Pdf.SetPageDimensions(595, 842);          // A4 in points
    FontID := Pdf.AddStandardFont(0);         // Helvetica
    Pdf.SelectFont(FontID);

    // Layer 1: i sottoservizi, disegnati e poi assegnati al proprio gruppo
    Pdf.SetTextColor(0.10, 0.30, 0.65);
    Pdf.DrawText(72, 770, 'Utilities: water main, valve chamber');
    UtilLayer := Pdf.NewOptionalContentGroup('Utilities');
    Pdf.SetContentStreamOptional(UtilLayer);
    Pdf.SetOptionalContentGroupVisible(UtilLayer, 1);   // visibile a schermo
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // e su carta

    // Layer 2: le correzioni del revisore su una pagina nuova
    Pdf.InsertPages(2, 1);                     // aggiunge una pagina dopo la pagina 1
    Pdf.SetTextColor(0.80, 0.10, 0.10);
    Pdf.DrawText(72, 770, 'REVIEW: revise valve spec before issue');
    RedlineLayer := Pdf.NewOptionalContentGroup('Reviewer markup');
    Pdf.SetContentStreamOptional(RedlineLayer);
    Pdf.SetOptionalContentGroupVisible(RedlineLayer, 1);    // visibile durante la revisione
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // never printed

    Pdf.SaveToFile('SitePlan_Layers.pdf');
  finally
    Pdf.Free;
  end;
end;

Il livello di revisione è il caso che merita attenzione. Viene mostrato a schermo per consentire a un revisore di vedere la nota, ma il suo flag di stampa è impostato a zero, così che una copia stampata dello stesso file non conterrà il testo della revisione. Questa asimmetria è l'unico motivo per cui si tengono separati i due stati

Rileggere la configurazione

La lettura dei livelli è un percorso diverso all'interno della medesima struttura. Dopo il caricamento di un file, GetOptionalContentConfigCount indica quanti dizionari di configurazione contiene il documento; la prima configurazione predefinita corrisponde all'ID 1. All'interno di una configurazione, GetOptionalContentConfigOrderCount fornisce il numero di voci nell'albero dell'ordine, indicizzate a partire da 1. Per ciascuna voce, GetOptionalContentConfigOrderItemLabel restituisce il testo visualizzato e GetOptionalContentConfigOrderItemLevel ne restituisce la profondità di nidificazione; in questo modo è possibile ricostruire esattamente la struttura del pannello con i sotto-livelli rientrati sotto le relative intestazioni

Ogni voce ha anche un tipo. GetOptionalContentConfigOrderItemType distingue un vero optional content group da una semplice etichetta di testo che ha la sola funzione di intestazione per una sezione dell'albero. Questa distinzione è importante perché le query di stato hanno senso solo per i gruppi reali. Per una voce di gruppo, GetOptionalContentConfigState segnala se la configurazione lo avvia come attivo, disattivo o invariato, e GetOptionalContentConfigLocked indica se all'utente è impedito di modificarne lo stato. Il ciclo seguente genera l'albero con lo stato e la condizione di blocco di ciascun gruppo, applicando un rientro in base al livello

var
  Pdf: TPDFlib;
  Cfg, Count, I, ItemType, GroupID, Indent: Integer;
  Line: string;
begin
  Pdf := TPDFlib.Create(nil);
  try
    if Pdf.LoadFromFile('SitePlan_Layers.pdf', '') = 0 then Exit;
    if Pdf.GetOptionalContentConfigCount = 0 then Exit;

    Cfg := 1;                                  // la configurazione di default
    Count := Pdf.GetOptionalContentConfigOrderCount(Cfg);
    for I := 1 to Count do
    begin
      Indent := Pdf.GetOptionalContentConfigOrderItemLevel(Cfg, I);
      Line := StringOfChar(' ', Indent * 2)
              + Pdf.GetOptionalContentConfigOrderItemLabel(Cfg, I);

      ItemType := Pdf.GetOptionalContentConfigOrderItemType(Cfg, I);
      if ItemType = 1 then                     // 1 = gruppo di contenuto opzionale (OCG)
      begin
        GroupID := Pdf.GetOptionalContentConfigOrderItemID(Cfg, I);
        case Pdf.GetOptionalContentConfigState(Cfg, GroupID) of
          1: Line := Line + '  [on]';
          2: Line := Line + '  [off]';
          3: Line := Line + '  [unchanged]';
        end;
        if Pdf.GetOptionalContentConfigLocked(Cfg, GroupID) = 1 then
          Line := Line + ' (locked)';
      end;
      // ItemType = 2 è un'etichetta di testo; non ha stato per gruppo

      Writeln(Line);
    end;
  finally
    Pdf.Free;
  end;
end;

Due dettagli mantengono corretto questo ciclo. L'indice dell'ordine inizia da uno, da 1 fino al conteggio, rispecchiando il modo in cui la libreria numera internamente l'albero. Inoltre, le chiamate specifiche per gruppo vengono eseguite solo quando il tipo di elemento è un gruppo, poiché un'etichetta di testo è solo un'intestazione con nome e livello ma non ha uno stato attivo, disattivo o bloccato da verificare. Evitare questo controllo comporterebbe la richiesta di uno stato a un'etichetta che ne è priva

Ciclo di rilettura delle configurazioni di contenuto opzionale nella PDF Library for Delphi: enumerare le config, percorrere le voci order a base uno, poi biforcare su ItemType così solo i gruppi reali vengono interrogati per stato on, off o locked mentre le etichette di testo vengono saltate
Rileggere i layer percorre le voci d'ordine a base uno e consulta lo stato solo per i gruppi reali, perché una semplice etichetta non ha uno stato on, off o locked da riferire

Integrazione

I livelli sono un meccanismo di presentazione, pertanto il motore deve rispettarli in ogni processo che esegue il rendering di una pagina; la parte di rendering è trattata nella nostra guida al rendering multi-motore in Delphi. Si intersecano inoltre con la struttura del documento, poiché il nome di un livello è testo visibile all'utente e il lettore trae vantaggio da una struttura a livelli organizzata, aspetto collegato al lavoro descritto nel nostro articolo sui PDF taggati e la struttura di accessibilità. Entrambi si affiancano alle API per il contenuto opzionale descritte qui, distribuite come parte della libreria PDF per Delphi insieme alle funzionalità per pagine, testo, font e conformità trattate altrove in questo blog