PDFium Component crea capas PDF nuevas desde Delphi a través de TPdf.SaveAsOptionalContentCreated. En un solo guardado verificado el método agrega grupos de optional content (OCGs), diccionarios de membresía de optional content (OCMDs) y dibujo suministrado por el llamador envuelto en Form XObjects cuya entrada /OC apunta a un grupo nuevo, así PDFium y otros visores muestran o esconden ese dibujo cuando la capa se conmuta
El pedido rara vez es abstracto. Un equipo de ingeniería de planta quiere dimensiones métricas e imperiales en la misma hoja de plano, con exactamente un conjunto visible. Una imprenta quiere una superposición de línea de corte que se vea en pantalla pero pueda apagarse antes de la prueba de imprenta. Ninguno de los dos archivos tiene todavía capas, así que no hay nada que conmutar. Conmutar capas existentes es otro trabajo, cubierto en conmutar capas de optional content PDF existentes con SaveAsOptionalContentConfigured; este artículo trata de crear las capas en primer lugar
¿Por qué no puede simplemente fijar /OC en un objeto de página con PDFium?
PDFium no tiene función pública para adjuntar optional content a un objeto de página existente. Los headers públicos no contienen FPDFPageObj_SetOC ni nada equivalente, así que la ruta usual de editar y luego FPDFPage_GenerateContent no puede producir contenido barrado. La compuerta tiene que escribirse en la estructura del archivo, y la única pregunta es qué estructura respetará PDFium al renderizar
ISO 32000-1 §8.11.3 da tres formas de hacer opcional el grafismo, y PDFium las maneja de forma distinta:
- Secciones de content stream de página envueltas en marked content
/OC /Name BDC...EMC, verificadas por objeto de página durante el renderizado - Form XObjects e image XObjects que llevan una entrada
/OCen su propio diccionario, verificadas cuando el XObject se pinta - Anotaciones con una entrada
/OCen el diccionario de anotación, que la ruta de renderizado de PDFium no consulta en absoluto, así que una anotación barrada sigue visible sin importar el estado de la capa
Envolver en marked content significa reescribir los content streams existentes operador por operador, y las anotaciones quedan descartadas por el renderer. Eso deja la ruta del Form XObject, y es la que usa PDFium Component: su dibujo entra en un Form XObject nuevo con /OC apuntando al grupo, el form se registra en el diccionario /Resources /XObject de la página, y un nuevo content stream que contiene un único /Name Do se anexa al /Contents de la página. El contenido existente de la página nunca se parsea ni se reescribe
Describir grupos y forms barrados en Delphi
TPdf.SaveAsOptionalContentCreated toma tres arreglos y devuelve un reporte. Ambas sobrecargas, una que escribe a un TStream y otra a un nombre de archivo, comparten la misma lista de parámetros: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport, y un AllowSignedDocument: Boolean = False opcional. El método llegó en PDFium Component v3.154.0
Cada TPdfOptionalContentCreateGroup tiene un Name, un Creator opcional, un Intent de pociView o pociDesign, un flag InitialStateVisible, un flag Locked y un RadioTag. Los grupos que comparten el mismo RadioTag caen en un arreglo /RBGroups, que es como un visor sabe que encender uno apaga a los demás. Cada TPdfOptionalContentCreateForm nombra su grupo barrador por GroupIndex (un índice base 0 dentro de su arreglo Groups), un PageNumber base 1, los bytes crudos del content stream, un bounding box, una matriz opcional y un ResourceName opcional
var
Pdf: TPdf;
Groups: TPdfOptionalContentCreateGroups;
Forms: TPdfOptionalContentCreateForms;
Report: TPdfOptionalContentCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'sheet-a1.pdf';
Pdf.Active := True;
SetLength(Groups, 2); // SetLength llena los registros de ceros
Groups[0].Name := 'Dimensions (mm)';
Groups[0].Intent := pociView;
Groups[0].InitialStateVisible := True;
Groups[0].RadioTag := 'Units';
Groups[1].Name := 'Dimensions (in)';
Groups[1].Intent := pociView;
Groups[1].InitialStateVisible := False;
Groups[1].RadioTag := 'Units';
SetLength(Forms, 2);
Forms[0].GroupIndex := 0;
Forms[0].PageNumber := 1;
Forms[0].Content := TEncoding.ASCII.GetBytes(
'0 0 1 RG 1.5 w 72 120 m 540 120 l S');
Forms[0].BBoxRight := 612;
Forms[0].BBoxTop := 792;
Forms[0].ResourceName := 'DimMetric';
Forms[1] := Forms[0];
Forms[1].GroupIndex := 1;
Forms[1].Content := TEncoding.ASCII.GetBytes(
'1 0 0 RG 1.5 w 72 140 m 540 140 l S');
Forms[1].ResourceName := 'DimImperial';
if not Pdf.SaveAsOptionalContentCreated('sheet-a1-layers.pdf',
Groups, nil, Forms, Report) then
raise Exception.Create(Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Note que el contenido del form es dibujo vectorial puro. El componente escribe solo un diccionario /Resources vacío en cada form, así que el content stream debe ser autocontenido: los colores de dispositivo, los trazados, el recorte y las transformaciones funcionan, pero cualquier cosa que necesite un recurso con nombre (una fuente para Tf, una imagen para Do, un ExtGState para gs) no tiene contra qué resolver. Si deja ResourceName vacío, los forms se llaman FmOc1, FmOc2 y así; un nombre personalizado puede contener solo letras, dígitos, guion bajo, guion y punto
¿Qué agrega un OCMD sobre un grupo simple?
Un diccionario de membresía de optional content hace que la visibilidad dependa de varios grupos a la vez. ISO 32000-1 §8.11.2.2 define dos formas: una lista /OCs con una política /P como /AnyOn o /AllOn, y una expresión de visibilidad /VE construida de /And, /Or y /Not según la especificación. PDFium Component toma un TPdfOptionalContentCreateMembership con MemberGroupIndices, un Policy y un Expression; cuando Expression.GroupIndices no está vacío, la expresión gana y /VE se escribe junto a /OCs
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Visible cuando cualquiera de las capas de unidades está activa: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Visible solo mientras la capa imperial está apagada: /VE [ /Not ref ]
SetLength(Memberships[1].MemberGroupIndices, 1);
Memberships[1].MemberGroupIndices[0] := 1;
Memberships[1].Expression.Kind := poceNot;
SetLength(Memberships[1].Expression.GroupIndices, 1);
Memberships[1].Expression.GroupIndices[0] := 1;
end;
El subconjunto soportado es deliberadamente angosto. Las políticas se limitan a ocmpAllOn y ocmpAnyOn; ocmpAnyOff y ocmpAllOff se rechazan. Las expresiones son de un nivel de profundidad, con poceAll, poceAny o poceNot sobre grupos creados, y /Not exige exactamente un índice de grupo. poceAll se serializa como /And, poceAny como /Or y poceNot como /Not, los únicos tres operadores que §8.11.2.2 define. Las versiones anteriores a 3.181.2 escribían /All y /Any, que PDFium evalúa como oculto, así que si creó OCMDs con una compilación vieja, regenérelos en lugar de confiar en la expresión. Cada membresía también necesita al menos una entrada en MemberGroupIndices, incluso cuando hay una expresión, porque esa lista se convierte en el arreglo /OCs. Los números de objeto de los nuevos diccionarios vuelven en Report.CreatedMembershipObjectNumbers
Dos comportamientos vale la pena conocer antes de construir sobre OCMDs. Primero, los forms barrados en la misma llamada referencian solo grupos: TPdfOptionalContentCreateForm.GroupIndex indexa Groups, no Memberships, así que un form no puede barrarse por un OCMD recién creado a través de esta API. Segundo, PDFium Component agrega cada OCMD nuevo al arreglo /OCProperties /OCGs del catálogo, mientras que /ON, /OFF y /Order se quedan solo para grupos. ISO 32000-1 §8.11.4.2 describe /OCGs como la lista de grupos, pero sin ese registro TPdf.InspectOptionalContent no enumera la membresía en absoluto. El efecto lateral es que un inventario de la salida lista cada OCMD como una entrada extra sin nombre en Inventory.Groups, así que iguale grupos por Name en lugar de por posición
¿Cómo parcha el componente un /OCProperties existente?
TPdf.SaveAsOptionalContentCreated construye /OCProperties desde cero o extiende el que ya está en el catálogo, y extiende anexando, nunca reconstruyendo. Un documento sin capas recibe un diccionario nuevo con /OCGs y una configuración por defecto /D que lleva /ON, /OFF y /Order, más /Locked y /RBGroups cuando algún grupo los pide, como se establece en ISO 32000-1 §8.11.4.3
Para un documento que ya tiene capas, cada arreglo de destino se parcha insertando las nuevas referencias justo antes de su corchete de cierre. Eso suena quisquilloso, y está ahí por una razón. Un arreglo /Order puede contener arreglos anidados y etiquetas de texto que moldean el árbol de capas que un visor muestra, y un enfoque anterior que aplanaba y reconstruía el arreglo perdía esas etiquetas. Insertar al final mantiene el árbol existente intacto y simplemente lista los grupos nuevos después de él. Los arreglos que viven en sus propios objetos indirectos se resuelven y parchan en el lugar
Desde v3.175.0 la configuración por defecto misma también puede ser indirecta. Algunos productores escriben /OCProperties << /OCGs [...] /D 12 0 R >>, y PDFium Component ahora sigue esa referencia y parcha el diccionario de destino en lugar de fallar con un error de entrada malformada. Los diccionarios /Resources de página directos e indirectos, incluido un subdiccionario /XObject indirecto, se manejan de la misma manera
Probar que las capas nuevas realmente conmutan
Una capa que existe en /OCProperties pero no barra nada es fácil de producir y difícil de notar. Durante el desarrollo la primera compilación del escritor de forms envolvía un objeto de stream que ya traía su propio header N 0 obj y trailer endobj dentro de un segundo header. PDFium no podía parsear el objeto duplicado, y en lugar de reportar un error se saltaba en silencio el operador Do. El archivo abría, el panel de capas se veía bien, y el dibujo barrado nunca aparecía. Lo que lo expuso fue una sonda que conmutaba la capa y comparaba píxeles renderizados: sin diferencia, así que algo andaba mal
Por eso el método verifica antes de entregar. La salida se reabre a través de una carga estricta que revisa la tabla de referencias cruzadas, y cada grupo solicitado se relee a través de InspectOptionalContent para confirmar su nombre, su estado inicial y su registro de bloqueo. Solo entonces la salida se copia a su stream, y la sobrecarga de archivo escribe un archivo temporal y reemplaza el destino atómicamente. Puede correr la misma verificación de conmutación por su cuenta reabriendo la salida, encontrando el grupo por nombre y guardando una copia con el estado opuesto
function SaveImperialPreset(const SourceName, TargetName: string): Boolean;
var
Pdf: TPdf;
Inventory: TPdfOptionalContentInventory;
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
CfgReport: TPdfOptionalContentConfigureReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourceName;
Pdf.Active := True;
if not Pdf.InspectOptionalContent(Inventory) then
raise Exception.Create(Inventory.ErrorMessage);
for I := 0 to High(Inventory.Groups) do
if (Inventory.Groups[I].Name = 'Dimensions (mm)') or
(Inventory.Groups[I].Name = 'Dimensions (in)') then
begin
SetLength(Changes, Length(Changes) + 1);
Changes[High(Changes)].GroupObjectNumber := Inventory.Groups[I].ObjectNumber;
Changes[High(Changes)].Visible := Inventory.Groups[I].Name = 'Dimensions (in)';
end;
Options := TPdfOptionalContentConfigureOptions.Default;
Result := Pdf.SaveAsOptionalContentConfigured(TargetName, Changes,
Options, CfgReport);
finally
Pdf.Free;
end;
end;
Renderice la página uno de ambos archivos y las líneas azul y roja deberían intercambiarse. Si no lo hacen, inspeccione primero el valor EffectiveVisible del grupo en el inventario; refleja el estado que PDFium realmente renderizará después de aplicar los arreglos de configuración y las reglas de uso
Límites y trampas para planear alrededor
TPdf.SaveAsOptionalContentCreated es un perfil acotado, y la mayoría de las sorpresas vienen de tratarlo como un editor de capas general. Las restricciones de abajo se aplican de entrada, así que una llamada que las viole falla con Report.ErrorMessage fijado y no escribe salida:
- Se exige al menos un grupo; grupos, membresías y forms tienen tope de 4096 entradas cada uno, los nombres de grupo deben ser únicos y de 1 a 255 caracteres, y cada form acepta a lo sumo 16 MiB de bytes de contenido
- Los documentos cifrados se rechazan, y un documento firmado necesita
AllowSignedDocument = True; la salida es una reescritura completa, así que las firmas existentes ya no verificarán - Cada entrada de
Formsse vuelve su propio Form XObject, así que poner la misma superposición en 200 páginas guarda el contenido 200 veces en lugar de una - El nuevo stream
Dose anexa después del contenido existente de la página, así que corre en el estado de gráficos que los streams anteriores dejen atrás; las páginas bien formadas restauran su estado, pero una página conq/Qsin balancear puede desplazar su superposición - El contenido existente de la página nunca se mueve a una capa; solo el dibujo que usted pasa en
Formsqueda barrado
El punto de repetición es el que hay que sopesar para documentos grandes. Si la superposición es pesada y se repite en cada página, el enfoque de objeto compartido de sellos de página reutilizables construidos sobre Form XObjects conserva una sola copia en el archivo, al costo de manejar usted la compuerta de capa
FAQ
¿Puede PDFium Component meter contenido de página existente en una capa nueva?
No. TPdf.SaveAsOptionalContentCreated barra solo el contenido que usted suministra en Forms; no envuelve operadores existentes en marked content ni los mueve a un XObject. Para poner en capas arte existente, regenérelo como contenido de form autocontenido y elimine el dibujo original en un paso aparte
¿Por qué mis anotaciones barradas siguen visibles cuando la capa está apagada?
PDFium no revisa la entrada /OC de los diccionarios de anotación en su ruta de renderizado, así que el barrado a nivel de anotación no tiene efecto en visores basados en PDFium. Esa es la razón por la que PDFium Component adjunta el contenido de capa a través de Form XObjects, que PDFium sí barra
¿Qué números de objeto debo usar al conmutar las capas nuevas más adelante?
Report.CreatedGroupObjectNumbers contiene los números en el archivo que acaba de escribirse. Cuando reabra ese archivo y lo configure, tome ObjectNumber de TPdf.InspectOptionalContent y empareje por Name, que sigue correcto incluso si un guardado posterior renumera objetos
PDFium Component trae SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured y InspectOptionalContent como un solo kit de optional content para Delphi y C++Builder, construido sobre el renderer PDFium. Detalles y descargas están en la página de producto de PDFium Component