Un topógrafo abre un plano de sitio y quiere los contornos ocultos mientras los servicios públicos permanecen encendidos. Un revisor quiere que las anotaciones en línea roja sean visibles en la pantalla y desaparezcan en la impresión. Una ficha de producto se envía en tres idiomas desde un solo archivo, y el lector elige qué idioma se muestra. Los tres son la misma función de PDF, y el panel que los impulsa en Acrobat se llama Capas (Layers). La característica debajo de ese panel es el contenido opcional, y es lo que permite que una sola página lleve varios estratos visuales independientes que un visor enciende y apaga
El contenido opcional se especifica en la norma ISO 32000-1 §8.11. La unidad de visibilidad es un grupo de contenido opcional, un OCG, un diccionario de tipo /OCG que lleva un nombre. El contenido marcado en una página está asociado con un grupo, y el visor decide si ese grupo se muestra actualmente. Una construcción relacionada, el diccionario de pertenencia de contenido opcional o OCMD, permite que la visibilidad dependa de una combinación booleana de varios grupos, pero el caso cotidiano es un solo grupo nombrado que representa una sola capa. El documento une todo el mecanismo a través de una entrada de catálogo, /OCProperties, que se describe a continuación
Lo que el catálogo tiene que llevar
Un OCG por sí solo es inerte. Para que un visor liste una capa y recuerde su estado, el catálogo de documentos necesita un diccionario /OCProperties, y la sección §8.11.4 expone exactamente qué va en él. Hay un arreglo /OCGs nombrando a cada grupo en el archivo, y hay una entrada /D que contiene la configuración predeterminada. La configuración predeterminada es la parte que un lector aplica cuando el archivo se abre por primera vez. Registra qué grupos comienzan encendidos y cuáles comienzan apagados, qué entradas están bloqueadas contra la manipulación del usuario y, a través de un arreglo /Order, cómo los nombres de las capas están dispuestos y anidados en el panel
La consecuencia práctica es que crear una capa nunca es un acto puramente local. El grupo tiene que ser dibujado en la página, y también tiene que ser registrado en una estructura a nivel de catálogo que no existía previamente. PDFlibPas hace ambos por usted. La primera llamada que hace un grupo agrega la entrada /OCProperties al catálogo y siembra la configuración predeterminada, de modo que la capa se pinta y se enumera sin requerir un registro contable (bookkeeping) separado de su lado
Por qué un modo de cumplimiento puede retener la característica
Antes de que se ejecute cualquier código de capa, el objetivo de conformidad del documento decide si el contenido opcional es siquiera legal. PDF/A-1, el perfil de archivo definido en ISO 19005-1, prohíbe la entrada /OCProperties directamente en §6.1.13. El razonamiento se ajusta al propósito del formato. Un archivo para preservación (archival) debe renderizarse idénticamente 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 la construcción en lugar de permitir un archivo ambiguo. PDF/A-2 y PDF/A-3, definidos en ISO 19005-2 e ISO 19005-3, toman el punto de vista opuesto en su §6.9 y permiten contenido opcional, con reglas sobre la visibilidad predeterminada
Esa diferencia aparece directamente en la API. Cuando el documento está en un modo PDF/A-1, NewOptionalContentGroup se niega a crear el grupo y devuelve cero, porque honrar la solicitud produciría un archivo que falla su propia conformidad declarada. En el modo PDF/A-2 o PDF/A-3, y en PDF ordinario sin restricciones, la misma llamada tiene éxito y devuelve un ID de grupo distinto de cero. Un resultado cero, por lo tanto, no es un fallo genérico para inspeccionar más tarde; es la biblioteca diciéndole que el nivel de cumplimiento activo no tiene espacio para la característica
var
Pdf: TPDFlib;
LayerID: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1); // PDF/A-1a: OCProperties prohibido
LayerID := Pdf.NewOptionalContentGroup('Utilities');
if LayerID = 0 then
// rechazado bajo PDF/A-1; no es un error transitorio, el modo prohíbe las capas
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 es simplemente visible o invisible. La configuración predeterminada registra su estado en pantalla y un estado de impresión separado, porque la sección §8.11.4 distingue lo que muestra un visor de lo que emite un flujo (pipeline) de impresión. Los dos son independientes a propósito. Una marca de agua de borrador puede mostrarse en la pantalla y omitirse en el papel, y una capa de línea de corte puede estar oculta en la pantalla y aun así ser enviada a un trazador (plotter). Colapsar los dos obligaría a uno a seguir al otro y perder exactamente el control que la característica pretende dar
PDFlibPas expone el par a través de dos establecedores (setters). SetOptionalContentGroupVisible toma el ID del grupo y una bandera, donde uno significa visible y cero significa oculto, y rige el estado predeterminado en pantalla. SetOptionalContentGroupPrintable toma el ID del grupo y una bandera para indicar si la capa se emite cuando se imprime el documento. Los captadores (getters) correspondientes, GetOptionalContentGroupVisible y GetOptionalContentGroupPrintable, cada uno devuelve uno o cero, de modo que usted puede leer la disposición de pantalla e impresión de una capa por separado en lugar de inferir uno del otro
Construyendo dos capas en una página
Crear una capa y llenarla sigue un orden fijo. Usted dibuja el contenido para la capa en la página actual, luego llama a SetContentStreamOptional con el ID de grupo, que envuelve el flujo de contenido actual de la página para que todo lo dibujado hasta ahora pertenezca a ese grupo. Dado que la llamada captura lo que haya en el flujo (stream) en ese momento, la disciplina es asentar las marcas de una capa, asignarlas, y solo entonces comenzar la siguiente capa. El siguiente ejemplo coloca los servicios públicos en la primera página y una línea roja (redline) de revisión en una segunda página, establece el estado de impresión y pantalla de cada capa, y guarda
var
Pdf: TPDFlib;
FontID, UtilLayer, RedlineLayer: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument; // PDF sin restricciones: capas permitidas
Pdf.SetPageDimensions(595, 842); // A4 en puntos
FontID := Pdf.AddStandardFont(0); // Helvetica
Pdf.SelectFont(FontID);
// Capa 1: servicios públicos, dibujados y luego asignados a su propio grupo
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); // mostrado en pantalla
Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // y en papel
// Capa 2: línea roja del revisor en una página nueva
Pdf.InsertPages(2, 1); // añadir una página después de la página 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 mientras se revisa
Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0); // nunca se imprime
Pdf.SaveToFile('SitePlan_Layers.pdf');
finally
Pdf.Free;
end;
end;
La capa de línea roja (redline) es el caso digno de notar. Se muestra en pantalla para que un revisor vea la nota, y su bandera de imprimible es cero, de modo que una copia impresa del mismo archivo no lleva el texto de revisión. Esa asimetría es precisamente la idea de mantener los dos estados separados
Leyendo la configuración
Leer capas es un paseo diferente a través de la misma estructura. Después de cargar un archivo, GetOptionalContentConfigCount reporta cuántos diccionarios de configuración contiene el documento; la primera configuración predeterminada es el ID de configuración 1. Dentro de una configuración, GetOptionalContentConfigOrderCount da el número de entradas en el árbol de orden (order tree), y usted las indexa a partir de 1. Para cada entrada, GetOptionalContentConfigOrderItemLabel devuelve su texto de visualización y GetOptionalContentConfigOrderItemLevel devuelve su profundidad de anidamiento, de modo que un contorno de panel con subcapas con sangría bajo los encabezados se puede reconstruir al pie de la letra
Cada entrada también tiene un tipo. GetOptionalContentConfigOrderItemType distingue un grupo de contenido opcional real de una etiqueta de texto simple que solo existe para encabezar una sección del árbol. Esa distinción importa porque las consultas de estado por grupo solo tienen sentido para grupos reales. Para una entrada de grupo, GetOptionalContentConfigState reporta si la configuración lo inicia encendido (on), apagado (off) o lo deja sin cambios, y GetOptionalContentConfigLocked reporta si el usuario tiene prohibido alternarlo (toggling). El bucle a continuación renderiza el árbol de orden con el estado y el estado de bloqueo de cada grupo, sangrando 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; // la configuración predeterminada
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 = grupo de contenido opcional
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 es un encabezado de etiqueta de texto; no tiene estado por grupo
Writeln(Line);
end;
finally
Pdf.Free;
end;
end;
Dos detalles mantienen correcto a este bucle. El índice de orden está basado en uno, de 1 al recuento, coincidiendo con cómo la biblioteca enumera 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 estado de encendido, apagado o bloqueado para consultar. Omita ese protector (guard) y le pedirá a una etiqueta un estado que no tiene
Dónde encaja esto
Las capas son un mecanismo de presentación, por lo que el motor tiene que respetarlas en cada ruta que renderiza una página, y el lado de la renderización se trata en nuestro recorrido paso a paso de la renderización multimotor en Delphi. También se cruzan con la estructura del documento, porque el nombre de una capa es un texto dirigido al autor y un lector se beneficia de un esquema de capa estructurado, que se conecta con el trabajo en nuestro artículo sobre PDF etiquetado y estructura de accesibilidad. Ambos se combinan con las APIs de contenido opcional descritas aquí, que se envían como parte de la Biblioteca PDF para Delphi junto con las utilidades de página, texto, fuente y conformidad discutidas en otras partes de este blog