Un topógrafo abre un plano de obra y desea ocultar las curvas de nivel mientras los servicios públicos permanecen activos. Un revisor desea que las anotaciones de marcado sean visibles en pantalla y desaparezcan de la impresión. Una ficha de producto se distribuye en tres idiomas dentro de un mismo archivo, y el lector elige cuál mostrar. Los tres casos corresponden a la misma característica de PDF, y el panel que los controla en Acrobat se denomina Capas. La característica subyacente a ese panel es el contenido opcional, que es lo que permite que una sola página contenga varios estratos visuales independientes que el visor enciende y apaga.
El contenido opcional se especifica en ISO 32000-1 §8.11. La unidad de visibilidad is un grupo de contenido opcional, o OCG por sus siglas en inglés, un diccionario de tipo /OCG que contiene un nombre. El contenido marcado en una página se asocia con un grupo, y el visor decide si ese grupo se muestra actualmente. Un concepto relacionado, el diccionario de pertenencia a contenido opcional u OCMD, permite que la visibilidad dependa de una combinación booleana de varios grupos, pero el caso cotidiano es un único grupo nombrado que representa una sola capa. El documento vincula todo el mecanismo a través de una sola entrada en el catálogo, /OCProperties, que se describe a continuación.
Lo que el catálogo debe contener
Un OCG por sí solo es inerte. Para que un visor enumere una capa y recuerde su estado, el catálogo del documento necesita un diccionario /OCProperties, y §8.11.4 detalla exactamente qué debe contener. Hay un arreglo /OCGs que nombra a cada grupo en el archivo, y hay una entrada /D que contiene la configuración predeterminada. La configuración predeterminada es la sección que un lector aplica cuando se abre el archivo por primera vez. Registra qué grupos inician activos y cuáles inactivos, qué entradas están bloqueadas para evitar que el usuario las alterne y, a través de un arreglo /Order, cómo se organizan y anidan los nombres de las capas en el panel.
La consecuencia práctica es que crear una capa nunca es un acto puramente local. El grupo debe dibujarse en la página y también debe registrarse en una estructura a nivel de catálogo que no existía previamente. PDFlibPas hace ambas cosas por usted. La primera llamada que crea un grupo agrega la entrada /OCProperties al catálogo e inicializa la configuración predeterminada, de modo que la capa se dibuja y se enumera sin necesidad de llevar un registro separado por su parte.
Por qué un modo de conformidad puede restringir esta característica
Antes de que se ejecute cualquier código de capa, el objetivo de conformidad del documento determina si el contenido opcional es legal. PDF/A-1, el perfil de archivo histórico definido en ISO 19005-1, prohíbe la entrada /OCProperties por completo en su sección §6.1.13. El razonamiento se ajusta al propósito del formato. Un archivo de preservación histórica debe renderizarse de forma idéntica para cada lector en el futuro lejano, y el contenido cuya visibilidad un visor puede cambiar es contenido cuya apariencia no es fija, por lo que el perfil prohíbe el concepto en lugar de permitir un archivo ambiguo. PDF/A-2 o PDF/A-3, definidos en ISO 19005-2 e ISO 19005-3, adoptan la postura opuesta en su sección §6.9 y permiten el contenido opcional, con reglas sobre la visibilidad predeterminada.
Esa diferencia se refleja directamente en la API. Cuando el documento está en modo PDF/A-1, NewOptionalContentGroup rechaza la creación del grupo y devuelve cero, porque atender la solicitud produciría un archivo que fallaría en su propia conformidad declarada. En el modo PDF/A-2 o PDF/A-3, y en un PDF común sin restricciones, la misma llamada tiene éxito y devuelve un ID de grupo distinto de cero. Por lo tanto, un resultado de cero no es una falla genérica que deba inspeccionar más tarde; es la biblioteca indicándole que el nivel de conformidad activo no admite esta característica.
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;
Dos estados por capa, no uno
Una capa no está simplemente visible o invisible. La configuración predeterminada registra su estado en pantalla y un estado de impresión separado, ya que §8.11.4 distingue lo que muestra el visor de lo que emite una cola de impresión. Ambos son independientes a propósito. Una marca de agua de borrador puede mostrarse en pantalla y omitirse en el papel, y una capa de líneas de corte puede ocultarse en pantalla pero enviarse a un plóter. Colapsar ambos estados obligaría a uno a seguir al otro, perdiendo exactamente el control que esta característica ofrece.
PDFlibPas expone este par a través de dos funciones de asignación. SetOptionalContentGroupVisible recibe el ID de grupo y una bandera, donde uno significa visible y cero significa oculto, y gobierna el estado en pantalla predeterminado. SetOptionalContentGroupPrintable recibe el ID de grupo y una bandera que indica si la capa se emite cuando el documento se imprime. Las funciones de lectura correspondientes, GetOptionalContentGroupVisible y GetOptionalContentGroupPrintable, devuelven uno o cero, de modo que puede consultar la disposición de pantalla e impresión de una capa por separado en lugar de inferir una a partir de la otra.
Construir dos capas en una página
Crear una capa y llenarla sigue un orden fijo. Se dibuja el contenido para la capa en la página actual, luego se llama a SetContentStreamOptional con el ID del grupo, lo cual envuelve el flujo de contenido actual de la página para que todo lo dibujado hasta ese momento pertenezca a ese grupo. Debido a que la llamada captura lo que esté en el flujo en ese instante, la regla es trazar las marcas de una capa, asignarlas y solo entonces comenzar con la siguiente capa. El ejemplo a continuación coloca los servicios públicos en la primera página y el marcado de revisión en una segunda página, establece el estado de pantalla e impresión de cada capa, y guarda.
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;
La capa de marcas de revisión es el caso digno de mención. Se muestra en pantalla para que el revisor vea la nota, pero su bandera de impresión es cero para que la versión impresa del mismo archivo no contenga el texto de la revisión. Esa asimetría es la razón fundamental para mantener separados ambos estados.
Leer la configuración de vuelta
Leer capas es un recorrido diferente a través de la misma estructura. Después de cargar un archivo, GetOptionalContentConfigCount informa cuántos diccionarios de configuración contiene el documento; la primera configuración predeterminada corresponde al ID de configuración 1. Dentro de una configuración, GetOptionalContentConfigOrderCount proporciona la cantidad de entradas en el árbol de orden, y se indexan desde 1. Para cada entrada, GetOptionalContentConfigOrderItemLabel devuelve su texto en pantalla y GetOptionalContentConfigOrderItemLevel devuelve su nivel de anidamiento, lo que permite reconstruir de forma idéntica un esquema de panel con subcapas sangradas bajo encabezados.
Cada entrada también tiene un tipo. GetOptionalContentConfigOrderItemType distingue un grupo de contenido opcional real de una simple etiqueta de texto que existe solo para encabezar una sección del árbol. Esa distinción es importante porque las consultas de estado por grupo solo tienen sentido para grupos reales. Para una entrada de grupo, GetOptionalContentConfigState informa si la configuración inicia en encendido, apagado o si se mantiene sin cambios, y GetOptionalContentConfigLocked informa si el usuario tiene prohibido alternarla. El bucle a continuación renderiza el árbol de orden con el estado y la condición de bloqueo de cada grupo, aplicando sangría por nivel.
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;
Dos detalles mantienen correcto este bucle. El índice de orden es de base uno, del 1 al total, coincidiendo con la forma en que la biblioteca numera el árbol internamente. Y las llamadas por grupo se ejecutan solo cuando el tipo de elemento es un grupo, porque una etiqueta de texto es un encabezado con un nombre y un nivel, pero sin estados de encendido, apagado o bloqueo que consultar. Omitir esa protección provocaría que se consulte a una etiqueta por un estado que no posee.
Dónde encaja esto
Las capas son un mecanismo de presentación, por lo que el motor debe respetarlas en cada ruta que renderiza una página; esta sección de renderizado se cubre en nuestra guía práctica sobre el renderizado de múltiples motores en Delphi. También se cruzan con la estructura del documento, porque el nombre de una capa es texto visible para el autor y el lector se beneficia de un esquema de capas estructurado, lo cual se conecta con el trabajo en nuestro artículo sobre PDF etiquetado y estructura de accesibilidad. Ambos se complementan con las API de contenido opcional descritas aquí, que se distribuyen como parte de la biblioteca PDF para Delphi junto con las características de páginas, texto, fuentes y conformidad tratadas en otras secciones de este blog.