Artículo técnico

Capas de contenido opcional PDF: conmutarlas en Delphi

PDFium Component controla las capas de contenido opcional PDF (OCG) en Delphi mediante dos métodos de TPdf: InspectOptionalContent lista cada capa junto con la visibilidad que PDFium va a renderizar de verdad, y SaveAsOptionalContentConfigured escribe una copia verificada en la que las capas que elijas quedan activadas o desactivadas. El segundo método además neutraliza las reglas Usage y /AS que de otro modo desharían tu edición en silencio. Ambos trabajan sobre el documento ya abierto en TPdf, así que no hay un segundo parser que mantener sincronizado con lo que muestra el visor

La petición suele llegar de un despacho de CAD o GIS: el juego de planos se entrega con cotas, anotaciones y un cajetín en capas separadas, y el cliente quiere una copia con las cotas ocultas antes de mandársela a un proveedor. PDFium renderiza el contenido opcional correctamente, pero su ABI pública no tiene ninguna función para enumerar OCG, elegir una configuración o voltear el estado de una capa. Así que bajas al nivel de objetos, editas /OCProperties, guardas, recargas, y la capa sigue ahí. La razón es la lógica de visibilidad de PDFium, y merece la pena entenderla antes de tocar ningún byte

¿Por qué editar /ON y /OFF no cambia lo que PDFium renderiza?

Editar los arrays /ON y /OFF del diccionario de configuración no basta, porque PDFium deja que un estado explícito dentro del diccionario /Usage del propio OCG gane a esos arrays, y una regla de auto-estado /AS puede después ganar a ambos. ISO 32000-1 §8.11.4 describe configuraciones y diccionarios de uso como mecanismos separados; el renderizador de PDFium los pliega en una sola decisión, y InspectOptionalContent la reproduce en este orden:

  • Parte del /BaseState de la configuración, donde /ON y /Unchanged cuentan ambos como visible y solo /OFF oculta
  • Aplica el array /ON de la configuración y después su array /OFF, de modo que un grupo listado en ambos acaba oculto
  • Aplica el estado Usage explícito del grupo para el uso pedido, como /Usage << /View << /ViewState /OFF >> >>, que gana a todo lo anterior
  • Trata como visible un grupo cuyo /Intent no contenga ni /View ni /All, porque no participa en la visibilidad de intent de vista
  • Por último ejecuta el array /AS de la configuración seleccionada, cuyas entradas para el evento correspondiente fijan el estado de los grupos que listan
La decisión de visibilidad en cinco pasos que PDFium Component reproduce para cada grupo de contenido opcional PDF en Delphi: BaseState fija el inicio, los arrays ON y OFF de la configuración se aplican en orden, una entrada Usage explícita ViewState o PrintState gana a ambos, la no participación por Intent cuenta como visible, y el array AS corre el último
Editar los arrays ON y OFF no basta porque PDFium pliega BaseState, ambos arrays, el estado Usage del grupo y por último las reglas de auto-estado AS en un solo veredicto que InspectOptionalContent reproduce paso a paso

El tercer paso es el que quema a la gente. Un archivo guardado por una herramienta de maquetación suele llevar /ViewState /ON en cada OCG, y entonces PDFium ignora tu array /OFF editado con tanto cuidado: el guardado tiene éxito, el archivo se reabre limpio, y la capa sigue pintándose. Para Print y Export, OcExplicitUsageState lee primero PrintState o ExportState y cae a ViewState cuando la entrada específica no está, así que un solitario ViewState /ON clava la capa también para imprimir. El marked content que referencia un OCMD (§8.11.2.2) se resuelve después contra estos resultados por grupo, mediante la política /P o, cuando está, la expresión de visibilidad /VE

¿Cómo listas las capas que PDFium va a mostrar de verdad?

TPdf.InspectOptionalContent devuelve un TPdfOptionalContentInventory cuyo array Groups lleva el número de objeto, el nombre, los intents, los tres estados Usage, el idioma, el rango de zoom, el flag Locked, el índice de radio-group y el EffectiveVisible calculado de cada OCG. El método hace primero que PDFium guarde el documento actual en memoria, expande los object streams y escanea el resultado, así que las ediciones hechas antes en la sesión se reflejan. El índice de configuración 0 es siempre el diccionario por defecto /D y las entradas de /Configs siguen a partir del índice 1; el argumento por defecto de -1 selecciona el índice 0. Un documento sin /OCProperties hace que el método devuelva False con el motivo en ErrorMessage en lugar de lanzar

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  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;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

El array Memberships reporta cada OCMD con su Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), el texto crudo de VisibilityExpression y su propio EffectiveVisible. Unas cuantas reglas de borde son deliberadas. /P vale /AnyOn por defecto, y un OCMD sin grupos cuenta como visible. Una referencia a un número de objeto que no es un OCG conocido se trata como visible en lugar de tumbar la expresión entera. La evaluación de /VE se para a una profundidad de anidamiento de 32 y trata todo lo más profundo como oculto, lo que evita que una expresión hostil o autorreferente convierta la inspección en un stack overflow

Escribir un nuevo estado de capas con SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured toma un array de records TPdfOptionalContentStateChange (número de objeto del grupo más Visible) y escribe un documento en el que la configuración seleccionada produce exactamente ese estado. La configuración seleccionada recibe /BaseState /ON más arrays /ON y /OFF completos que cubren cada grupo, y cada OCG que ya tiene diccionario Usage recibe un ViewState explícito (o PrintState / ExportState, según Options.Usage) que coincide con su nuevo estado. Con TPdfOptionalContentConfigureOptions.Default, la clave /AS de la configuración seleccionada se elimina para que un evento open, print o export no pueda voltear las capas de vuelta

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
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;

  // Configuración 0, ocuView, DisableAutomaticState y EnforceRadioGroups a True
  Options := TPdfOptionalContentConfigureOptions.Default;

  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;

El camino de escritura conserva la salida guardada del propio PDFium como prefijo byte a byte y solo añade 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 un solo byte 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 la tabla de referencias cruzadas no valida. La sobrecarga de archivo va un paso más allá: escribe a un archivo temporal junto al destino y reemplaza el destino solo cuando la verificación tiene éxito, así que una actualización rechazada jamás deja un plano escrito a medias. Es el mismo enfoque de revisión incremental verificada que usa el editor de name tree y number tree de PDF en PDFium Component

Cómo escribe SaveAsOptionalContentConfigured en PDFium Component un PDF de Delphi con capas conmutadas: los cambios de estado y las opciones entran, la configuración seleccionada se reescribe con arrays ON y OFF completos y estados Usage, la revisión incremental verificada se añade, y una reapertura estricta debe validar antes de escribir nada
El guardado configurado conserva el re-save del propio PDFium como prefijo de bytes, añade el propietario de configuración reescrito más una nueva sección xref, y reabre el resultado en un TPdf separado antes de tocar el destino

¿A qué se niega el guardado configurado?

El guardado configurado se niega a cualquier cambio que el propio documento prohíba o no pueda representar con seguridad, y cada negativa ocurre antes de tocar el destino. Un número de objeto que no está en /OCGs falla sin más. Cambiar un grupo listado en el array /Locked de la configuración falla, aunque reafirmar su valor actual está permitido. Con EnforceRadioGroups activo, cualquier conjunto /RBGroups que acabara con más de un miembro visible se rechaza en lugar de apagar a los demás en silencio. Los documentos cifrados se rechazan porque los objetos incrementales en plaintext no pueden llevar el security handler activo. Los documentos firmados lanzan EPdfError salvo que pases AllowSignedDocument = True, ya que cambiar lo que muestra una página puede romper la cobertura de la firma o una política de certificación

Las puertas de denegación que SaveAsOptionalContentConfigured aplica en PDFium Component antes de escribir un PDF configurado en Delphi: un número de objeto fuera de los OCG falla, los grupos bloqueados fallan, los conjuntos RBGroups con más de un miembro visible se rechazan, los documentos cifrados no pueden llevar objetos incrementales en plaintext, y los archivos firmados exigen AllowSignedDocument
Cada negativa ocurre antes de tocar el destino, y el motivo del fallo aterriza en Report.ErrorMessage en lugar de dejar un plano escrito a medias
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // escribe /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // primera entrada de /Configs, no /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument sigue a False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // archivo firmado: nada escrito en Target
      Result := False;
    end;
  end;
end;

Conoce los trade-offs antes de cablear esto en un trabajo por lotes. La revisión añadida se apoya en el re-save completo de PDFium, no en los bytes de tu archivo original, que es exactamente por lo que la entrada firmada necesita consentimiento explícito. La reescritura además normaliza la configuración seleccionada a /BaseState /ON, así que una línea base /Unchanged o /OFF del autor se sustituye por arrays explícitos con la misma visibilidad resultante. Quitar /AS elimina trucos solo de impresión, como una capa de marca de agua que aparece únicamente en papel; pon DisableAutomaticState a False para conservar esas reglas, aceptando que pueden ganar a tu estado pedido para ese evento. En el lado bueno, 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 los diccionarios de configuración, así que la salida por defecto elimina una incidencia que tu validación preflight PDF/A con PDFium Component si no reportaría

Dónde encaja el control de capas en un visor PDF en Delphi

En un visor, el control de capas es una checklist alimentada por el inventario más una recarga del resultado guardado. Rellena la checklist desde Groups, deshabilita las entradas que estén Locked, trata como mutuamente excluyentes los miembros que compartan RadioGroupIndex, y al aplicar escribe a un TMemoryStream y carga ese stream de vuelta en TPdf para que la vista pinte el nuevo estado. El cableado entre TPdf y TPdfView está cubierto en cómo construir un visor PDF completo con PDFium VCL en Delphi. Licencias, descargas de prueba y el resto del conjunto de características están en la página de producto de PDFium Component para Delphi