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.
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. PDFlibPas 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
// refused under PDF/A-1; not a transient error, the mode bans layers
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.
PDFlibPas 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.
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: utilities, drawn then assigned to its own group
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); // shown on screen
Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper
// Layer 2: reviewer redline on a fresh page
Pdf.InsertPages(2, 1); // append one page after page 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); // visible while reviewing
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; // the default configuration
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 = optional content group
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 is a text label heading; it has no per-group state
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.
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.