Artículo técnico

Reescribir flujos de contenido PDF de forma segura en Delphi

HotPDF recorre cada operador del flujo de contenido de una página y entrega cada uno a su código mediante VisitLoadedPageContentOperators. Para cada operador usted elige una acción: conservarlo, eliminarlo, reemplazarlo con sus propios bytes, detener el recorrido, o rechazar toda la edición. Cuando se conservan todos los operadores, el flujo reescrito es idéntico byte a byte al original, propiedad que hace seguro ejecutar el mecanismo sobre documentos que usted no creó

La edición de flujos de contenido es donde las herramientas de PDF suelen fallar. Un flujo es una secuencia de operadores sobre una máquina de pila, no un árbol de documento, y el enfoque ingenuo de ejecutar una expresión regular sobre los bytes decodificados se rompe en el momento en que un literal de cadena contiene algo que parece un operador, o una imagen en línea contiene binario arbitrario. Un visitor que ya ha tokenizado el flujo no tiene ese problema

Operadores de flujo de contenido PDF como BT, Tf y TJ que fluyen hacia los datos del evento del visitor con fase, nombre del operador, cantidad de operandos, span de bytes y estado de anidamiento, para luego devolver una de cinco acciones: conservar, reemplazar, eliminar, detener o rechazar
Cada operador visitado produce exactamente una decisión, y una pasada que conserva todo escribe de vuelta los rangos intactos para obtener una salida idéntica byte a byte

¿Qué se puede hacer realmente con acceso a nivel de operador?

Tres familias de tareas. Auditoría: contar cuántas veces una página establece un color fuera de un conjunto conocido, o encontrar texto dibujado con una fuente que no está embebida. Eliminación: borrar una marca de agua que se dibujó como contenido marcado, o quitar una invocación de imagen específica. Y normalización: reemplazar una forma de operador por un equivalente que un consumidor posterior maneje mejor

Cada una de esas tareas comparte un requisito: usted debe poder dejar intacto el otro 99 por ciento del flujo. El árbol de sintaxis de HotPDF guarda una copia de la fuente más spans compactos, y las ediciones son overlays ordenados y sin superposición. Al escribir el resultado se copian directamente los rangos intactos y solo se emiten bytes de reemplazo donde usted cambió algo, de modo que un ida y vuelta sin cambios no puede introducir desviaciones

El contrato del visitor

Implemente la interfaz o asigne el evento, y luego llame al método de visita con ApplyChanges en false para una auditoría o en true para una reescritura. Los datos del evento llevan la fase, el nombre del operador, su cantidad de operandos, su span de bytes, el estado de anidamiento actual y una vista prestada para leer los operandos:

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // Contar operadores de despliegue de texto dibujados dentro de contenido marcado
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Leer un operando solo cuando se necesite
  if EventData.OperatorName = 'Do' then
    if EventData.View.GetOperandBytes(0, Bytes) then
      NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));

  EventData.Action := coaKeep;
end;

procedure TAuditor.Run(const FileName: string);
var
  Pdf: THotPDF;
  Stats: THPDFContentOperatorVisitStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;
    Pdf.OnContentOperator := HandleOperator;
    for I := 0 to Pdf.LoadedPageCount - 1 do
      Pdf.VisitLoadedPageContentOperators(I, False, Stats);   // solo auditoría
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

¿Por qué la vista es prestada y no mutable?

El callback nunca recibe un árbol de sintaxis modificable. Recibe una vista que puede obtener los bytes de la operación actual o un operando específico bajo demanda, y eso es todo. En cambio, todo cambio pasa por los campos de acción y de reemplazo

Esa indirección no es mera formalidad. La mutación directa del árbol permitiría que un visitor evadiera el presupuesto de reemplazo y la verificación de ediciones superpuestas, y las ediciones superpuestas en un modelo de overlay por spans de bytes producen una salida silenciosamente incorrecta en lugar de romperse de forma evidente. Enrutar los cambios a través del campo de acción hace que cada edición se valide de la misma manera, sin importar qué haya intentado el visitor

Leer bajo demanda también mantiene barato el caso común. Una auditoría que solo cuenta nombres de operadores nunca materializa los bytes de operando en absoluto, lo cual importa en un flujo de contenido con cientos de miles de operaciones

El campo de estado describe siempre la entrada

Cada evento lleva la profundidad del estado gráfico, la profundidad del objeto de texto, la profundidad del contenido marcado y la profundidad de la sección de compatibilidad tal como estaban en la entrada original justo antes de que se ejecutara ese operador. Eso es cierto incluso si su visitor eliminó o reemplazó operadores anteriores

Anclar el estado a la entrada original en lugar de a la salida editada es una decisión deliberada con un beneficio práctico: ejecutar la misma auditoría dos veces sobre el mismo documento produce resultados idénticos, y una auditoría que corre junto con ediciones no desplaza su propio marco de referencia. Si necesita conocer la profundidad en la salida, usted sabe qué eliminó, así que puede calcularla; lo contrario, reconstruir posiciones de entrada estables a partir de una salida cambiante, no es posible

Eliminar y reemplazar, con las reglas que lo protegen

Un reemplazo consiste en bytes crudos de flujo de contenido, y usted es responsable de su sintaxis. Las salvaguardas a su alrededor son el presupuesto de bytes de reemplazo totales, el requisito de no superposición, y la validación opcional de salida que vuelve a analizar el resultado antes de aceptarlo:

Salvaguardas de reescritura en Delphi: un visitor que declara la intención de Action y Replacement, enrutada a través del presupuesto de reemplazo, el requisito de span de bytes sin superposición y las validaciones de salida, hacia la página guardada validada
Todo cambio se canaliza a través del campo de acción hacia las validaciones de presupuesto, superposición y reanálisis, de modo que una validación fallida aborta en lugar de escribir operadores rotos
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Eliminar los operadores que pintan el formulario de la marca de agua
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Normalizar un color extraviado al gris interno
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Rechazar toda la edición si encontramos algo que no entendemos
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // volver a analizar antes de aceptar
  Options.MaxReplacementBytes := 64 * 1024;

  Pdf.OnContentOperator := HandleOperator;
  if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
    if Stats.Modified then
      Pdf.SaveLoadedDocument('cleaned.pdf');
end;

El conjunto de acciones incluye dos salidas por una razón. coaStop termina el recorrido y conserva lo decidido hasta ese punto, lo cual es correcto cuando usted ya encontró lo que buscaba. coaReject abandona toda la visita y deja la página intacta, lo cual es correcto cuando encuentra una construcción que sus reglas de reescritura no cubren. Las imágenes en línea son el ejemplo clásico: su carga binaria no es algo alrededor de lo cual un visitor normalizador de color deba estar editando

Cómo leer las estadísticas, y dónde se detiene el recorrido

El campo de estado distingue la finalización de la detención, el rechazo, la entrada inválida, un límite excedido, la salida inválida y un error del visitor. Esos son problemas distintos: salida inválida significa que sus bytes de reemplazo no se pudieron volver a analizar, mientras que entrada inválida significa que el flujo de origen ya estaba mal formado y no se intentó ninguna edición

Los contadores hacen que una auditoría se autoverifique. VisitedOperatorCount comparado con KeptOperatorCount, DeletedOperatorCount y ReplacedOperatorCount debe cuadrar, y las cifras de profundidad máxima le indican si la página anida lo suficiente como para merecer una revisión más de cerca. SourceBytes comparado con OutputBytes le da en un solo número el efecto de tamaño de su edición

Conciliación de las estadísticas de auditoría, donde los conteos de operadores conservados, eliminados y reemplazados suman el total visitado, junto a siete finales de estado, desde completado hasta error del visitor, sobre una nota de que las profundidades de estado describen la entrada original, de modo que las auditorías se repiten de forma idéntica
Contadores que deben cuadrar, más un estado que nombra cada final posible, hacen que una auditoría se autoverifique en documentos que usted no creó

Para las tareas comunes existen rutas de más alto nivel que no requieren escribir un visitor en absoluto. Eliminar contenido de forma permanente, con la limpieza de recursos que necesita una redacción real, se cubre en redacción y unión de páginas en documentos cargados. Reemplazar texto visible se cubre en buscar y reemplazar texto en un PDF existente. Recurra al visitor de operadores cuando su regla sea estructural en lugar de textual, o cuando necesite una auditoría que ningún informe existente responda

El recorrido, la extracción y la reescritura de flujos de contenido operan sobre el mismo modelo de documento cargado en Delphi y C++Builder; la lista completa de funciones está en la página del componente PDF para Delphi de HotPDF