Un juego de planos llega con sus cotas en una capa propia, y la forma obvia de ocultarlas es mover la referencia de ese grupo del arreglo /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 de título. La causa está a un objeto de distancia, dentro del diccionario del grupo: /Usage << /View << /ViewState /ON >> >>, un estado explícito que el renderizador lee antes que los arreglos de configuración
El estado de una capa puede vivir en varios lugares de un PDF, y una edición de /OCProperties toca solo uno de ellos. El motor PDFium de Google renderiza el 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 encender y apagar un grupo. El código Delphi que maneja capas tiene que reescribir los objetos por su cuenta y reproducir exactamente el orden de decisión del renderizador, o su lista de capas reporta un estado mientras la página muestra otro
¿Por qué editar /ON y /OFF no cambia lo que PDFium renderiza?
Los arreglos /ON y /OFF son solo el segundo de cinco pasos, y dos pasos posteriores pueden revocarlos. 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 que §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 se ve así:
- Partir del
/BaseStatede la configuración, donde/ONy/Unchangedcuentan ambos como visible y solo/OFFoculta - Aplicar el arreglo
/ONde 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
/Intentno contiene ni/Viewni/All, ya que no participa en la visibilidad de intent de vista - Finalmente correr el arreglo
/ASde la configuración seleccionada, cuyas entradas para el evento coincidente fijan el estado de los grupos que listan
El tercer paso explica el archivo de la introducción: algunos exportadores escriben /ViewState /ON en cada grupo que crean, y desde entonces ninguna edición de arreglos oculta esas capas. Impresión y exportación siguen la misma regla con un giro: el renderizador lee primero /PrintState o /ExportState y recurre a /ViewState cuando falta la entrada específica. Un /ViewState /ON solitario fija entonces la capa también en papel. El cuarto paso es más silencioso. Un grupo etiquetado /Intent /Design queda por completo fuera de la visibilidad de intent de vista y se pinta sin importar los arreglos, 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 llena un TPdfOptionalContentInventory y devuelve True en caso de éxito; su parámetro Usage viene por defecto en ocuView y su parámetro ConfigurationIndex en -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 hace que PDFium guarde el documento abierto en memoria y expande cualquier object stream, así que las ediciones hechas antes en la sesión quedan reflejadas en el resultado. Un archivo sin /OCProperties, o un índice de configuración fuera del rango disponible, hace que la llamada devuelva False con la razón en ErrorMessage; no se lanza ninguna excepción en ninguno de los dos casos
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
Cfg: TPdfOptionalContentConfiguration;
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;
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 el arreglo /OFF, usted ha 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 /RBGroups
El resto del diccionario Usage se reporta como el archivo lo declara: Language y LanguagePreferred, UserType y UserNames, PageElementSubtype, y MinimumZoom y MaximumZoom detrás de una bandera HasZoomRange. EffectiveVisible no depende de ninguno de ellos. El ajuste de pantalla según el zoom es cosa de su visor. Cada TPdfOptionalContentConfiguration expone su nombre, BaseState, contenidos de arreglos y RadioGroups, más OrderGroupObjectNumbers, que aplana /Order en referencias de grupo y se salta las etiquetas de texto de los arreglos de etiquetas anidados
Impresión y exportación pueden discrepar con la pantalla
El argumento Usage cambia la respuesta, y de eso se trata que exista. Bajo ocuPrint el inventario lee /PrintState con su recurso a /ViewState, y solo toman parte las entradas /AS cuyo /Event es /Print. Una capa de marca de agua construida para aparecer solo en papel reporta por tanto oculta bajo ocuView y visible bajo ocuPrint. Comparar las dos pasadas encuentra esas capas antes de que una impresión le sorprenda a alguien
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 el mismo arreglo /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 membresía de contenido opcional (OCMD, ISO 32000-1 §8.11.2.2) ata una pieza 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, deja que /VE tenga precedencia sobre /OCGs y /P cuando está presente, y dice que un OCMD sin grupos no tiene efecto sobre la visibilidad
El arreglo Memberships del inventario sigue esas reglas contra los resultados por grupo de la misma pasada, así que el uso que usted pidió se mantiene. Cada membresía reporta sus números de objeto de grupo, Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, u 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, de modo que una referencia colgante no oculta todo lo que la rodea. 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 cada OCMD guardado como objeto indirecto con /Type /OCMD, object streams incluidos
Escribir un nuevo estado de capa con SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured toma un arreglo de records TPdfOptionalContentStateChange, cada uno con un GroupObjectNumber y una bandera Visible. Escribe una copia en la que la configuración seleccionada produce exactamente ese estado bajo el orden de cinco pasos. Los grupos que usted no menciona conservan la visibilidad que el inventario computó 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 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 algo 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 esa carga no tiene éxito o la tabla de referencias cruzadas no valida. La sobrecarga de archivo escribe a un archivo temporal junto al destino y reemplaza el destino solo después de 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 de PDF en PDFium Component. Para confirmar un preset de impresión, recargue la salida y llame a InspectOptionalContent(Inv, ocuPrint, 1); los grupos deberían reportar los estados que usted pasó
¿A qué se niega el guardado configurado?
El guardado configurado se niega a cualquier cambio que el documento prohíba o no pueda cargar de forma segura, y cada negación ocurre antes de que el destino se escriba. Las comprobaciones corren en un orden fijo. Un documento firmado lanza EPdfError antes de que cualquier otra cosa pase, salvo que usted pase AllowSignedDocument = True. Un documento cifrado se rechaza después, porque los objetos incrementales en texto plano no pueden cargar 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 el arreglo /Locked de la configuración falla, aunque reafirmar su valor actual pasa. Al final, con EnforceRadioGroups activado, un conjunto /RBGroups que terminaría con más de un miembro visible se rechaza. Toda negación salvo la de firma devuelve False con la razón en Report.ErrorMessage
Dos de esas compuertas van más allá 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 opción como apagar a los demás miembros cuando uno se enciende. PDFium Component aplica ambos también a los cambios programáticos, y para los grupos de opción se niega en lugar de elegir miembros que apagar. Si le piden dos miembros de un conjunto mutuamente excluyente, la biblioteca no puede saber cuál usted quiso decir, y una adivinanza contradiría su petición. Apague a los demás en el mismo arreglo Changes y el guardado pasa
La compuerta de documento firmado existe porque la revisión agregada se monta sobre el re-guardado completo de PDFium, que es una secuencia de bytes nueva; el rango de bytes que cubre una firma existente no se puede asumir que sobreviva. Pase AllowSignedDocument = True solo cuando perder esa firma sea aceptable
Tres efectos secundarios 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 arreglos explícitos 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; ponga DisableAutomaticState en False para conservar esas reglas y acepte que pueden revocar el estado que usted pidió para su evento. Y los arreglos /ON y /OFF se escriben a partir de 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. Guarde los presets de impresión en una entrada de /Configs y deje /D para la pantalla. Del 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 los diccionarios de configuración, así que la salida por defecto elimina un hallazgo que su validación preflight PDF/A con PDFium Component de otro modo reportaría
Aplicar cambios de capa dentro de un visor Delphi
En un visor, el control de capas es una checklist construida a partir del inventario, seguida de un guardado en memoria y una recarga. Llene la checklist desde Groups, deshabilite las entradas cuya bandera Locked esté activa, trate como mutuamente excluyentes a los miembros que comparten un RadioGroupIndex, y al aplicar escriba a un TMemoryStream por la sobrecarga de stream. TPdf.LoadDocument espera un componente inactivo, así que apáguelo primero; la carga buferada copia los bytes, lo que le permite liberar el stream de inmediato
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 viene en 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 coinciden. El cableado entre TPdf y TPdfView está cubierto en cómo construir un visor PDF completo con PDFium VCL en Delphi. Cuando un documento todavía no tiene capas y usted necesita agregarlas, crear capas PDF con OCG y OCMD en Delphi recorre SaveAsOptionalContentCreated
Precios, la compilación de prueba y las demás funciones de documentos del componente están en la página de producto de PDFium Component para Delphi