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
¿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:
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
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