HotPDF recorre cada operador del flujo de contenido de una página y entrega cada uno a tu código mediante VisitLoadedPageContentOperators. Para cada operador eliges una acción: conservarlo, eliminarlo, sustituirlo por tus 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 no creaste tú
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 aplicar una expresión regular sobre los bytes decodificados se rompe en cuanto un literal de cadena contiene algo con aspecto de operador, o una imagen en línea contiene binario arbitrario. Un visitor que ya ha tokenizado el flujo no tiene ese problema
¿Qué puedes 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 localizar texto dibujado con una fuente que no está incrustada. Eliminación: borrar una marca de agua dibujada como contenido marcado, o extraer una invocación de imagen concreta. Y normalización: sustituir una forma de operador por un equivalente que un consumidor posterior maneje mejor
Las tres comparten un mismo requisito: debes poder dejar intacto el otro 99 % del flujo. El árbol de sintaxis de HotPDF almacena una única copia de la fuente más rangos compactos (spans), y las ediciones son overlays ordenados y no solapados. Al escribir el resultado se copian directamente los rangos sin tocar y solo se emiten bytes de sustitución donde cambiaste algo, de modo que un round trip sin cambios no puede introducir desviaciones
El contrato del visitor
Implementa la interfaz o asigna el evento, y luego llama 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 número de operandos, su rango de bytes, el estado de anidamiento actual y una vista prestada para leer operandos:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// Cuenta los operadores de mostrar 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;
// Lee un operando solo cuando lo necesites
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 concreto bajo demanda, y nada más. Cada cambio pasa en su lugar por los campos de acción y de sustitución
Esa indirección no es mero formalismo. Mutar el árbol directamente permitiría a un visitor saltarse el presupuesto de sustitución y la comprobación de ediciones solapadas, y las ediciones solapadas en un modelo de overlays por rango 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 forma, sin importar qué pretendiera el visitor
Leer bajo demanda también mantiene barato el caso común. Una auditoría que solo cuenta nombres de operadores nunca llega a materializar los bytes de los operandos, 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 tu visitor eliminó o sustituyó operadores anteriores
Anclar el estado a la entrada original, y no a la salida editada, es una decisión deliberada con una ventaja práctica: ejecutar la misma auditoría dos veces sobre el mismo documento produce resultados idénticos, y una auditoría que se ejecuta junto con ediciones no desplaza su propio marco de referencia. Si necesitas conocer la profundidad en la salida, sabes qué eliminaste, así que puedes calcularla; lo contrario, reconstruir posiciones de entrada estables a partir de una salida cambiante, no es posible
Eliminar y sustituir, con las reglas que te protegen
Una sustitución son bytes de flujo de contenido en bruto, y tú eres responsable de su sintaxis. Las salvaguardas que la rodean son el presupuesto sobre el total de bytes de sustitución, el requisito de no solapamiento, 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;
// Descarta los operadores que pintan el formulario de marca de agua
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// Normaliza un color suelto al gris corporativo
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// Rechaza 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; // vuelve 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 momento, lo cual es correcto cuando ya has encontrado lo que buscabas. coaReject abandona toda la visita y deja la página intacta, lo cual es correcto cuando encuentras una construcción que tus reglas de reescritura no cubren. Las imágenes en línea son el ejemplo clásico: su carga útil binaria no es algo que un visitor de normalización de color deba intentar editar alrededor
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 no válida, un límite superado, la salida no válida y un error del visitor. Son problemas distintos: salida no válida significa que tus bytes de sustitución no se pudieron volver a analizar, mientras que entrada no válida significa que el flujo de origen ya estaba malformado 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 te dicen si la página se anida lo suficiente como para merecer una mirada más de cerca. SourceBytes comparado con OutputBytes te da en un solo número el efecto de tamaño de tu edición
Para las tareas comunes existen rutas de nivel más alto que no requieren escribir un visitor en absoluto. Eliminar contenido de forma permanente, con la limpieza de recursos que exige una redacción real, se trata en redacción y unión de páginas en documentos cargados. Sustituir texto visible se trata en buscar y reemplazar texto en un PDF existente. Recurre al visitor de operadores cuando tu regla sea estructural en vez de textual, o cuando necesites 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 HotPDF Delphi PDF