Un juego de planos llega con sus cotas en una capa propia, y la manera obvia de esconderlas es mover la referencia de ese grupo de la matriz /ON de la configuración por defecto a /OFF. El archivo se guarda y se reabre, y las líneas de cota siguen impresas sobre el cajetín. La causa está a un objeto de distancia, dentro del diccionario de grupo: /Usage << /View << /ViewState /ON >> >>, un estado explícito que el renderizador lee por delante de las matrices de configuración
El estado de una capa puede vivir en varios sitios de un PDF, y una edición de /OCProperties solo toca uno de ellos. El motor PDFium de Google renderiza contenido opcional correctamente, pero su API C pública no tiene función para enumerar grupos de contenido opcional (OCG), elegir una configuración o conmutar un grupo dentro y fuera. El código Delphi que gobierna capas tiene que reescribir los objetos por su cuenta y reproducir exactamente el orden de decisión del renderizador, o su lista de capas informa un estado mientras la página muestra otro
¿Por qué editar /ON y /OFF no cambia lo que PDFium renderiza?
Las matrices /ON y /OFF son solo el segundo de cinco pasos, y dos pasos posteriores pueden revocarlas. ISO 32000-1 §8.11.4.3 define el diccionario de configuración de contenido opcional con /BaseState, /ON, /OFF, /Locked, /RBGroups y /AS, mientras §8.11.4.4 define el diccionario Usage que un OCG puede llevar. En la especificación, Usage describe para qué sirve un grupo, y las entradas de auto-estado en /AS convierten esa descripción en ON u OFF. PDFium toma un camino más corto: cuando renderiza para un propósito, gana el estado explícito del propio grupo para ese propósito. Plegado todo junto, el orden queda así:
- Parte del
/BaseStatede la configuración, donde/ONy/Unchangedcuentan ambos como visible y solo/OFFesconde - Aplica la matriz
/ONy después la matriz/OFF, de modo que un grupo listado en ambas acaba oculto - Aplica el estado Usage explícito del grupo para el propósito pedido, como
/Usage << /View << /ViewState /OFF >> >>, que se impone a todo lo anterior - Trata como visible un grupo cuyo
/Intentno contiene ni/Viewni/All, porque no participa en la visibilidad de intent de vista - Ejecuta al final la matriz
/ASde la configuración seleccionada, dejando que las entradas del evento coincidente fijen el estado de los grupos que listan
El paso tres explica el archivo del inicio: algunos exportadores escriben /ViewState /ON en cada grupo que crean, y desde entonces ninguna edición de matrices esconde esas capas. Imprimir y exportar siguen la misma regla con un giro: el renderizador lee /PrintState o /ExportState primero y cae a /ViewState cuando falta la entrada específica. Un solitario /ViewState /ON clava pues la capa también en papel. El paso cuatro es más silencioso. Un grupo etiquetado /Intent /Design está fuera de la visibilidad de intent de vista por completo y pinta sin caso de las matrices, mientras que un grupo sin clave /Intent se trata como /View, que es el valor por defecto que da la especificación
Listar capas y su estado renderizado con InspectOptionalContent
PDFium Component reproduce ese orden en TPdf.InspectOptionalContent. El método rellena un TPdfOptionalContentInventory y devuelve True si tiene éxito; su parámetro Usage vale ocuView por defecto y su parámetro ConfigurationIndex vale -1, que selecciona el índice 0. El índice de configuración 0 es el diccionario /D por defecto, y las entradas de /Configs siguen desde el índice 1 en orden de archivo. Antes de escanear, el método pide a PDFium que guarde el documento abierto en memoria y expande los object streams, así que las ediciones hechas antes en la sesión aparecen en el resultado. Un archivo sin /OCProperties, o un índice de configuración fuera del rango disponible, hace que la llamada devuelva False con el motivo en ErrorMessage; no se lanza excepción en ninguno de los dos casos
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
Cfg: TPdfOptionalContentConfiguration;
G: TPdfOptionalContentGroup;
begin
// Usage vale ocuView por defecto; -1 selecciona la configuración 0, el diccionario /D
if not Pdf.InspectOptionalContent(Inv) then
begin
Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
Exit;
end;
Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
[Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
BoolToStr(Cfg.HasAutomaticState, True)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %-24s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Cada TPdfOptionalContentGroup lleva su número de objeto, nombre, Intents y los tres estados explícitos ViewState, PrintState y ExportState, cada uno de ocsUnspecified, ocsOn u ocsOff. Cuando un estado lee ocsOn y el grupo está en la matriz /OFF, has encontrado la trampa del párrafo inicial. Locked y RadioGroupIndex vienen de la configuración seleccionada, y RadioGroupIndex es -1 para un grupo que no pertenece a ningún conjunto de /RBGroups
El resto del diccionario Usage se informa tal como lo declara el archivo: Language y LanguagePreferred, UserType y UserNames, PageElementSubtype, y MinimumZoom y MaximumZoom tras un indicador HasZoomRange. EffectiveVisible no depende de ninguno de ellos. La presentación dependiente del zoom es cosa de tu visor. Cada TPdfOptionalContentConfiguration expone su nombre, BaseState, contenido de matrices y RadioGroups, más OrderGroupObjectNumbers, que aplana /Order en referencias a grupos y se salta las etiquetas de texto de las matrices de etiquetas anidadas
Imprimir y exportar pueden discrepar de la pantalla
El argumento Usage cambia la respuesta, y para eso existe. Bajo ocuPrint el inventario lee /PrintState con su fallback a /ViewState, y solo participan las entradas de /AS cuyo /Event es /Print. Una capa de marca de agua construida para aparecer solo en papel informa pues oculta bajo ocuView y visible bajo ocuPrint. Comparar las dos pasadas encuentra esas capas antes de que una impresión sorprenda a nadie
procedure TFormMain.ReportPrintOnlyLayers;
var
OnScreen, OnPaper: TPdfOptionalContentInventory;
I: Integer;
begin
if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
Exit;
// Ambas pasadas leen la misma matriz /OCGs, así que el orden de grupos coincide
for I := 0 to High(OnScreen.Groups) do
if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
[string(OnScreen.Groups[I].Name),
BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;
¿Cómo se evalúan las expresiones de visibilidad OCMD?
Un diccionario de pertenencia de contenido opcional (OCMD, ISO 32000-1 §8.11.2.2) ata un trozo de contenido a varios grupos mediante una política /P, o mediante una expresión de visibilidad /VE que combina grupos con /And, /Or y /Not. La especificación da a /P un valor por defecto de /AnyOn, permite que /VE tome precedencia sobre /OCGs y /P cuando está presente, y dice que un OCMD sin grupos no tiene efecto sobre la visibilidad
La matriz Memberships del inventario sigue esas reglas contra los resultados por grupo de la misma pasada, así que el usage que pediste se propaga. Cada pertenencia informa sus números de objeto de grupo, Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, o ocmpUnknown para un nombre fuera de esos cuatro), el texto crudo de VisibilityExpression y su propio EffectiveVisible. Un OCMD sin grupos cuenta como visible. Una referencia a un número de objeto que no es un OCG conocido también cuenta como visible, así que una referencia colgante no esconde todo lo que hay alrededor. La evaluación de /VE se detiene a una profundidad de anidamiento de 32 y trata cualquier cosa más profunda como oculta, así que una expresión autorreferente no puede desbordar la pila. El escaneo lista todo OCMD guardado como objeto indirecto con /Type /OCMD, object streams incluidos
Escribir un nuevo estado de capa con SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured toma una matriz de registros TPdfOptionalContentStateChange, cada uno con un GroupObjectNumber y un indicador Visible. Escribe una copia en la que la configuración seleccionada produce exactamente ese estado bajo el orden de cinco pasos. Los grupos que no menciones conservan la visibilidad que el inventario calculó para ellos
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
ForPrint: Boolean);
var
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
SetLength(Changes, 2);
Changes[0].GroupObjectNumber := DimensionsObj;
Changes[0].Visible := False;
Changes[1].GroupObjectNumber := NotesObj;
Changes[1].Visible := True;
Options := TPdfOptionalContentConfigureOptions.Default;
if ForPrint then
begin
Options.Usage := ocuPrint; // escribe /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // primera entrada de /Configs, /D se queda como estaba
end;
if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
Changes, Options, Report) then
raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);
Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
[Report.ChangedGroupCount, Report.GroupCount,
Report.UpdatedUsageStateCount,
BoolToStr(Report.RemovedAutomaticState, True)]));
end;
La salida conserva el documento guardado por el propio PDFium como prefijo byte a byte y añade solo el propietario de configuración reescrito y los objetos OCG que llevan diccionarios Usage, seguidos de una nueva sección xref y un trailer. Antes de que nada llegue a tu destino, el resultado se reabre en un TPdf separado bajo la política de carga estricta, y el método falla si esa carga no tiene éxito o la tabla de referencias cruzadas no valida. La sobrecarga de archivo escribe a un archivo temporal junto al objetivo y sustituye el objetivo solo tras la verificación, así que una actualización rechazada deja el plano anterior intacto. Es el mismo enfoque de revisión incremental verificada que usa el editor de name trees y number trees PDF en PDFium Component. Para confirmar un preset de impresión, recarga la salida y llama a InspectOptionalContent(Inv, ocuPrint, 1); los grupos deberían informar los estados que pasaste
¿Qué se niega a hacer el guardado configurado?
El guardado configurado rechaza cualquier cambio que el documento prohíbe o no puede transportar con seguridad, y cada rechazo ocurre antes de escribir el destino. Las comprobaciones corren en un orden fijo. Un documento firmado lanza EPdfError antes de que ocurra nada más salvo que pases AllowSignedDocument = True. Un documento cifrado se rechaza después, porque los objetos incrementales en claro no pueden transportar el security handler activo. Luego se comprueba cada cambio pedido: un número de objeto que no está en /OCGs falla sin más, y un cambio a un grupo listado en la matriz /Locked de la configuración falla, aunque reafirmar su valor actual pasa. Al final, con EnforceRadioGroups activado, se rechaza un conjunto de /RBGroups que acabaría con más de un miembro visible. Cada rechazo salvo el de la firma devuelve False con el motivo en Report.ErrorMessage
Dos de esas compuertas van más lejos de lo que la especificación exige estrictamente. §8.11.4.3 describe /Locked como protección contra cambios a través de la interfaz de usuario del visor y describe los grupos de botones de radio como apagar a los demás miembros cuando uno se enciende. PDFium Component aplica ambos también a cambios programáticos, y para grupos de radio rechaza en lugar de elegir miembros que apagar. Pedidos dos miembros de un conjunto mutuamente excluyente, la biblioteca no puede saber cuál querías, y una apuesta contradiría tu petición. Apaga a los demás en la misma matriz Changes y el guardado pasa
La compuerta de documento firmado existe porque la revisión añadida se apoya en el re-save completo de PDFium, que es una nueva secuencia de bytes; no puede asumirse que el byte range que cubre una firma existente sobreviva. Pasa AllowSignedDocument = True solo cuando perder esa firma es aceptable
Tres efectos colaterales importan en un trabajo por lotes. La reescritura normaliza la configuración seleccionada a /BaseState /ON, así que una línea base /Unchanged o /OFF del autor se vuelve matrices explícitas con la misma visibilidad resultante. Soltar /AS también elimina trucos solo de impresión, como una capa de marca de agua que aparece únicamente en papel; pon DisableAutomaticState en False para conservar esas reglas y acepta que pueden imponerse a tu estado pedido para su evento. Y las matrices /ON y /OFF se escriben desde los estados calculados para Options.Usage, así que un preset de impresión guardado en la configuración 0 también cambia lo que la pantalla muestra para cada grupo sin diccionario Usage. Guarda los presets de impresión en una entrada de /Configs y deja el /D para la pantalla. En el lado del cumplimiento, PDF/A-2 (ISO 19005-2 cláusula 6.9) y PDF/UA (ISO 14289-1 cláusula 7.10) prohíben ambos /AS en diccionarios de configuración, así que la salida por defecto limpia un hallazgo que tu validación preflight PDF/A con PDFium Component en otro caso reportaría
Aplicar cambios de capa dentro de un visor Delphi
En un visor, el control de capas es una checklist construida desde el inventario, seguida de un guardado a memoria y una recarga. Rellena la checklist desde Groups, deshabilita las entradas cuyo indicador Locked está activo, trata como mutuamente excluyentes a los miembros que comparten RadioGroupIndex y, al aplicar, escribe a un TMemoryStream a través de la sobrecarga de stream. TPdf.LoadDocument espera un componente inactivo, así que apágalo primero; la carga buffered copia los bytes, lo que te permite liberar el stream enseguida
procedure TFormMain.ApplyLayerChecklist(
const Changes: TPdfOptionalContentStateChanges);
var
Report: TPdfOptionalContentConfigureReport;
Output: TMemoryStream;
KeepPage: Integer;
begin
Output := TMemoryStream.Create;
try
try
if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
TPdfOptionalContentConfigureOptions.Default, Report) then
begin
ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
Exit;
end;
except
on E: EPdfError do
begin
ShowMessage(E.Message); // archivo firmado: Output sigue vacío
Exit;
end;
end;
KeepPage := Pdf.PageNumber;
Pdf.Active := False;
Pdf.LoadDocument(Output); // Buffered vale True por defecto
Pdf.PageNumber := KeepPage;
finally
Output.Free;
end;
end;
Tras la recarga, PDFium pinta desde la nueva configuración, así que la pantalla y la siguiente llamada a InspectOptionalContent concuerdan. El cableado entre TPdf y TPdfView está cubierto en construir un visor PDF completo con PDFium VCL en Delphi. Cuando un documento aún no tiene capas y necesitas añadirlas, crear capas PDF con OCG y OCMD en Delphi recorre SaveAsOptionalContentCreated
Precios, la compilación de prueba y las demás características de documentos del componente están listadas en la página de producto de PDFium Component para Delphi