O HotPDF percorre cada operador do fluxo de conteúdo de uma página e entrega cada um deles ao seu código por meio de VisitLoadedPageContentOperators. Para cada operador você escolhe uma ação: mantê-lo, excluí-lo, substituí-lo pelos seus próprios bytes, interromper a travessia ou rejeitar a edição inteira. Quando todo operador é mantido, o fluxo reescrito é idêntico byte a byte ao original, propriedade que torna o mecanismo seguro para rodar sobre documentos que você não criou
A edição de fluxos de conteúdo é onde as ferramentas de PDF costumam errar. Um stream é uma sequência de operadores sobre uma máquina de pilha, não uma árvore de documento, e a abordagem ingênua de rodar uma expressão regular sobre os bytes decodificados quebra no momento em que um literal de string contém algo que parece um operador, ou uma imagem inline contém binário arbitrário. Um visitante que já tokenizou o stream não tem esse problema
O que você realmente pode fazer com acesso em nível de operador?
Três famílias de tarefas. Auditoria: contar quantas vezes uma página define uma cor fora de um conjunto conhecido, ou encontrar texto desenhado com uma fonte que não está incorporada. Remoção: excluir uma marca d'água que foi desenhada como conteúdo marcado, ou remover uma invocação de imagem específica. E normalização: substituir uma forma de operador por um equivalente que um consumidor posterior trate melhor
Cada uma delas compartilha um requisito: você precisa conseguir deixar os outros 99 por cento do stream intocados. A árvore de sintaxe do HotPDF armazena uma cópia da fonte mais spans compactos, e as edições são overlays ordenados e sem sobreposição. Gravar o resultado copia diretamente os intervalos intocados e emite bytes de substituição apenas onde você mudou algo, de modo que um round trip sem alterações não pode introduzir desvios
O contrato do visitante
Implemente a interface ou atribua o evento, depois chame o método de visita com ApplyChanges definido como false para uma auditoria ou true para uma reescrita. Os dados do evento carregam a fase, o nome do operador, sua contagem de operandos, seu span de bytes, o estado de aninhamento atual e uma view emprestada para ler os operandos:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// Conta operadores de exibição de texto desenhados dentro de conteúdo marcado
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// Lê um operando somente quando você precisa dele
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); // somente auditoria
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
Por que a view é emprestada, e não mutável?
O callback nunca recebe uma árvore de sintaxe modificável. Ele recebe uma view que pode buscar os bytes da operação atual ou um operando específico sob demanda, e é só isso. Toda mudança passa, em vez disso, pelos campos de ação e substituição
Essa indireção não é cerimônia. A mutação direta da árvore permitiria que um visitante contornasse o orçamento de substituição e a checagem de edições sobrepostas, e edições sobrepostas em um modelo de overlay por spans de bytes produzem uma saída silenciosamente errada, em vez de visivelmente quebrada. Encaminhar as mudanças pelo campo de ação significa que toda edição é validada da mesma forma, independentemente do que o visitante pretendia
Ler sob demanda também mantém o caso comum barato. Uma auditoria que apenas conta nomes de operadores nunca materializa bytes de operandos, o que importa em um fluxo de conteúdo com centenas de milhares de operações
O campo de estado descreve a entrada, sempre
Cada evento carrega a profundidade do estado gráfico, a profundidade do objeto de texto, a profundidade de conteúdo marcado e a profundidade da seção de compatibilidade tal como estavam na entrada original imediatamente antes daquele operador ser executado. Isso vale mesmo que seu visitante tenha excluído ou substituído operadores anteriores
Ancorar o estado à entrada original, em vez de à saída editada, é uma escolha deliberada com um ganho prático: rodar a mesma auditoria duas vezes sobre o mesmo documento produz resultados idênticos, e uma auditoria que roda junto com edições não desloca seu próprio referencial. Se você precisa saber a profundidade na saída, você sabe o que excluiu, então pode calculá-la; o inverso, reconstruir posições de entrada estáveis a partir de uma saída em movimento, não é possível
Excluindo e substituindo, com as regras que protegem você
Uma substituição são bytes brutos de fluxo de conteúdo, e você é responsável pela sintaxe deles. As salvaguardas em torno disso são o orçamento sobre o total de bytes de substituição, o requisito de não sobreposição, e a validação opcional de saída que reanalisa o resultado antes de ele ser aceito:
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// Descarta os operadores que pintam o formulário da marca d'água
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// Normaliza uma cor solta para o cinza padrão da casa
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// Rejeita a edição inteira se encontrarmos algo que não entendemos
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // reanalisa antes de aceitar
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;
O conjunto de ações inclui duas saídas por um motivo. coaStop encerra a travessia e mantém o que já foi decidido até então, o que é certo quando você encontrou o que estava procurando. coaReject abandona a visita inteira e deixa a página intocada, o que é certo quando você encontra uma construção que suas regras de reescrita não cobrem. Imagens inline são o exemplo clássico: sua carga útil binária não é algo em torno do qual um visitante de normalização de cor deveria estar editando
Lendo as estatísticas, e onde a travessia para
O campo de status distingue conclusão de interrupção, rejeição, entrada inválida, limite excedido, saída inválida e erro do visitante. Esses são problemas diferentes: saída inválida significa que seus bytes de substituição não foram reanalisados com sucesso, enquanto entrada inválida significa que o fluxo de origem já estava malformado e nenhuma edição foi tentada
Os contadores tornam uma auditoria autoverificável. VisitedOperatorCount comparado a KeptOperatorCount, DeletedOperatorCount e ReplacedOperatorCount precisa fechar a soma, e os valores de profundidade máxima dizem se a página tem aninhamento profundo o bastante para valer uma olhada mais atenta. SourceBytes comparado a OutputBytes dá o efeito de tamanho da sua edição em um único número
Para as tarefas comuns existem rotas de nível mais alto que não exigem escrever um visitante. Remover conteúdo permanentemente, com a limpeza de recursos que uma redação real exige, é abordado em redação e montagem de páginas em documentos carregados. Substituir texto visível é abordado em busca e substituição em um PDF existente. Recorra ao visitante de operadores quando sua regra for estrutural, e não textual, ou quando você precisar de uma auditoria que nenhum relatório existente responda
A visita, a extração e a reescrita de fluxos de conteúdo operam sobre o mesmo modelo de documento carregado no Delphi e no C++Builder; a lista completa de recursos está na página do componente PDF Delphi HotPDF