Un topógrafo abre un plano del sitio 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 en un solo archivo, y el lector elige qué idioma se muestra. Las tres son la misma característica de PDF, y el panel que las controla en Acrobat se llama Capas. La característica debajo de ese panel es el contenido opcional, y es lo que permite que una sola página contenga varios estratos visuales independientes que un visor activa y desactiva.
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 se asocia con un grupo, y el visor decide si ese grupo se muestra actualmente. Un constructo 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 con nombre que representa una sola capa. El documento identifica todo el mecanismo a través de una entrada de 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 el §8.11.4 detalla exactamente qué va en él. Hay una matriz /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 parte que aplica un lector cuando el archivo se abre por primera vez. Registra qué grupos comienzan activados y cuáles desactivados, qué entradas están bloqueadas para evitar que el usuario las alterne y, a través de una matriz /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 registrarse en una estructura a nivel de catálogo que no existía previamente. PDFlibPas hace ambos por usted. La primera llamada que crea un grupo añade la entrada /OCProperties al catálogo y siembra la configuración predeterminada, de modo que la capa se pinta y se enumera sin necesidad de llevar un registro separado por su parte.
Por qué un modo de cumplimiento puede retener la función
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 la norma ISO 19005-1, prohíbe la entrada /OCProperties por completo en el §6.1.13. El razonamiento se adapta al propósito del formato. Un archivo de almacenamiento debe representarse de manera idéntica para cada lector en el futuro, y el contenido cuya visibilidad puede cambiar un visor es contenido cuya apariencia no es fija, por lo que el perfil prohíbe el constructo en lugar de permitir un archivo ambiguo. PDF/A-2 y PDF/A-3, definidos en las normas ISO 19005-2 e ISO 19005-3, adoptan la postura opuesta en su §6.9 y permiten el contenido opcional, con reglas sobre la visibilidad predeterminada.
Esa diferencia se muestra directamente en la API. Cuando el documento está en modo PDF/A-1, NewOptionalContentGroup se niega a crear el 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 un fallo genérico para inspeccionar más tarde; es la librería que le indica que el nivel de cumplimiento activo no tiene espacio para la función.
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 es simplemente visible o invisible. La configuración predeterminada registra su estado en pantalla y un estado de impresión separado, porque el §8.11.4 distingue lo que muestra un visor de lo que emite un flujo de impresión. Ambos son independientes a propósito. Una marca de agua de borrador se puede mostrar en pantalla y omitir en el papel, y una capa de líneas de corte se puede ocultar en pantalla pero enviar a un plóter. Fusionar ambos obligaría a que uno siguiera al otro y se perdería exactamente el control para el cual existe la función.
PDFlibPas expone el par a través de dos métodos de asignación. SetOptionalContentGroupVisible toma el ID del grupo y una bandera, donde uno significa visible y cero oculto, y gobierna el estado predeterminado en pantalla. SetOptionalContentGroupPrintable toma el ID del grupo y una bandera para determinar si la capa se emite cuando se imprime el documento. Los métodos de lectura correspondientes, GetOptionalContentGroupVisible y GetOptionalContentGroupPrintable, devuelven uno o cero cada uno, de modo que puede leer la disposición de pantalla e impresión de una capa por separado en lugar de inferir una de la otra.
Construir dos capas en una página
Crear una capa y llenarla sigue un orden fijo. Dibuja el contenido para la capa en la página actual y luego llama a SetContentStreamOptional con el ID del grupo, lo que envuelve el flujo de contenido actual de la página de modo que todo lo dibujado hasta el momento preoceda a ese grupo. Debido a que la llamada captura lo que esté en el flujo en ese instante, la disciplina consiste en colocar las marcas de una capa, asignarlas y solo entonces comenzar la siguiente capa. El ejemplo a continuación coloca los servicios públicos en la primera página y un marcado de revisor 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 marcado es el caso que vale la pena notar. Se muestra en pantalla para que un revisor vea la nota, y su bandera imprimible es cero para que una copia impresa del mismo archivo no lleve texto de revisión. Esa asimetría es todo el propósito de mantener separados los dos 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 es el 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 de visualización y GetOptionalContentConfigOrderItemLevel devuelve su profundidad de anidamiento, de modo que se puede reconstruir palabra por palabra 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 etiqueta de texto plano que existe solo 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 informa si la configuración lo inicia activado, desactivado o lo deja sin cambios, y GetOptionalContentConfigLocked informa si el usuario tiene prohibido alternarlo. El siguiente bucle representa el árbol de orden con el estado de cada grupo y el estado de bloqueo, 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; // 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 este bucle correcto. El índice de orden está basado en uno, de 1 al recuento, coincidiendo con la forma en que la librería 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 estado de activado, desactivado o bloqueado que consultar. Omita esa protección 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 debe respetarlas en cada ruta que represente una página, y el lado de la representación se cubre en nuestro tutorial sobre representación multimotor en Delphi. También se cruzan con la estructura del documento, porque el nombre de una capa es texto orientado al autor y un lector se beneficia de un esquema de capa estructurado, lo que conecta con el trabajo en nuestro artículo sobre PDF etiquetado y estructura de accesibilidad. Ambos se emparejan con las API de contenido opcional descritas aquí, que se distribuyen como parte de la Librería PDF para Delphi junto con las funciones de página, texto, fuente y conformidad analizadas en otras partes de este blog.