Artículo técnico

Capas de contenido opcional de PDF con PDFium en Delphi

PDFium Component controla las capas de contenido opcional de PDF (OCG) en Delphi mediante dos métodos de TPdf: InspectOptionalContent lista cada capa junto con la visibilidad que PDFium realmente renderizará, y SaveAsOptionalContentConfigured escribe una copia verificada en la que las capas que usted elija quedan encendidas o apagadas. El segundo método además neutraliza las reglas Usage y /AS que de otro modo desharían su 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 el visor muestra

El pedido suele llegar de un despacho de CAD o GIS: el juego de planos sale con cotas, anotaciones y un cajetín de título 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 función para enumerar OCG, elegir una configuración o voltear el estado de una capa. Así que baja al nivel de objeto, edita /OCProperties, guarda, recarga, y la capa sigue ahí. La razón es la lógica de visibilidad de PDFium, y vale la pena entenderla antes de tocar un solo byte

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

Editar los arreglos /ON y /OFF del diccionario de configuración no basta, porque PDFium deja que un estado explícito dentro del propio diccionario /Usage del OCG gane sobre esos arreglos, y una regla de auto-estado /AS puede entonces superar 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:

  • Partir del /BaseState de la configuración, donde /ON y /Unchanged cuentan ambos como visible y solo /OFF oculta
  • Aplicar el arreglo /ON de la configuración, luego su arreglo /OFF, de modo que un grupo listado en ambos termine oculto
  • Aplicar el estado Usage explícito del grupo para el uso pedido, como /Usage << /View << /ViewState /OFF >> >>, que supera todo lo anterior
  • Tratar como visible un grupo cuyo /Intent no contiene ni /View ni /All, ya que no participa en la visibilidad de intent de vista
  • Finalmente correr el arreglo /AS de la configuración seleccionada, cuyas entradas para el evento coincidente fijan el estado de los grupos que listan
La decisión de visibilidad de cinco pasos que PDFium Component replica para cada grupo de contenido opcional de PDF en Delphi: BaseState fija el inicio, los arreglos ON y OFF de la configuración se aplican en orden, una entrada Usage explícita ViewState o PrintState supera a ambos, la no participación por Intent cuenta como visible, y el arreglo AS corre al final
Editar los arreglos ON y OFF no basta porque PDFium pliega BaseState, ambos arreglos, el estado Usage del grupo y finalmente 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 cargar /ViewState /ON en cada OCG, y PDFium entonces ignora su arreglo /OFF editado con tanto cuidado: el guardado tiene éxito, el archivo reabre limpio, y la capa sigue pintándose. Para Print y Export, OcExplicitUsageState lee primero PrintState o ExportState y cae de vuelta a ViewState cuando la entrada específica no está, así que un ViewState /ON solitario clava la capa también para impresión. El marked content que referencia un OCMD (§8.11.2.2) se resuelve luego contra estos resultados por grupo, mediante la política /P o, cuando existe, la expresión de visibilidad /VE

¿Cómo lista las capas que PDFium realmente mostrará?

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

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage viene por defecto en ocuView; -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 arreglo Memberships reporta cada OCMD con su Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), el texto crudo de VisibilityExpression y su propio EffectiveVisible. Algunas reglas de borde son deliberadas. /P viene por defecto en /AnyOn, 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 fallar toda la expresión. La evaluación de /VE se detiene a una profundidad de anidamiento de 32 y trata cualquier cosa más profunda como oculta, lo que evita que una expresión hostil o autorreferente convierta la inspección en un stack overflow

Escribir un nuevo estado de capa con SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured toma un arreglo 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 arreglos /ON y /OFF completos cubriendo cada grupo, y cada OCG que ya tiene un diccionario Usage recibe un ViewState explícito (o PrintState / ExportState, siguiendo Options.Usage) que corresponde a su nuevo estado. Con TPdfOptionalContentConfigureOptions.Default, la clave /AS de la configuración seleccionada se elimina para que un evento de apertura, impresión o exportación 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 en 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 agrega solo el dueño de configuración reescrito y los objetos OCG que cargan diccionarios Usage, seguidos de una nueva sección xref y el trailer. Antes de que un solo byte llegue a su 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 después de que la verificación tiene éxito, así que una actualización rechazada jamás deja un plano a medio escribir. Es el mismo enfoque de revisión incremental verificada que usa el editor de name trees y number trees de PDF en PDFium Component

Cómo SaveAsOptionalContentConfigured en PDFium Component escribe un PDF de Delphi con capas alternadas: entran los cambios de estado y las opciones, la configuración seleccionada se reescribe con arreglos ON y OFF completos y estados Usage, se agrega la revisión incremental verificada, y una reapertura estricta debe validar antes de escribirse nada
El guardado configurado conserva el re-guardado del propio PDFium como prefijo de bytes, agrega el dueño 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 de forma segura, y cada negación 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 arreglo /Locked de la configuración falla, aunque reafirmar su valor actual está permitido. Con EnforceRadioGroups activado, cualquier conjunto /RBGroups que termine 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 cargar el security handler activo. Los documentos firmados lanzan EPdfError salvo que usted pase AllowSignedDocument = True, ya que cambiar lo que una página muestra puede romper la cobertura de firma o una política de certificación

Las compuertas de negación que SaveAsOptionalContentConfigured aplica en PDFium Component antes de escribir un PDF de Delphi configurado: un número de objeto fuera de OCGs falla, los grupos bloqueados fallan, los conjuntos RBGroups con más de un miembro visible se rechazan, los documentos cifrados no pueden cargar objetos incrementales en plaintext, y los archivos firmados exigen AllowSignedDocument
Cada negación ocurre antes de tocar el destino, y la razón del fallo aterriza en Report.ErrorMessage en lugar de dejar un plano a medio escribir
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 en False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // archivo firmado: nada escrito al Target
      Result := False;
    end;
  end;
end;

Conozca los trade-offs antes de conectar esto a un trabajo por lotes. La revisión agregada se monta sobre el re-guardado completo de PDFium, no sobre los bytes originales de su archivo, que es exactamente por qué 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 queda reemplazada por arreglos explícitos con la misma visibilidad resultante. Soltar /AS elimina trucos solo de impresión como una capa de marca de agua que aparece únicamente en papel; ponga DisableAutomaticState en False para conservar esas reglas, aceptando que pueden superar el estado que usted pidió para ese evento. Del lado positivo, 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 un issue que su validación preflight PDF/A con PDFium Component de otro modo reportaría

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

En un visor, el control de capas es una checklist alimentada por el inventario más una recarga del resultado guardado. Llene la checklist desde Groups, deshabilite las entradas que estén Locked, trate como mutuamente excluyentes a los miembros que comparten un RadioGroupIndex, y al aplicar escriba a un TMemoryStream y cargue 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 set de funcionalidades están en la página de producto de PDFium Component para Delphi