Artigo Técnico

Estado de Content Stream no PDF Library for Delphi: Rastreamento de CTM e Clip

O PDF Library for Delphi, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder, reproduz o content stream de uma página por meio de sua classe TPDFContentStateTracker sem tocar em nenhum canvas de renderização. Alimentar o rastreador com um operador analisado por vez mantém um registro corrente de estado gráfico — matriz de transformação atual, matriz de texto, limites de clip, e a pilha de salvamento q/Q — disponível para um snapshot antes ou depois de cada operador executar

Pergunte onde uma sequência de texto de fato pousa na página impressa, e os números brutos do content stream sozinhos vão te enganar toda vez. TPDFContentProgram.GetTextRuns já reporta o ponto âncora de cada instrução de exibição de texto por meio dos campos OriginX e OriginY em TPDFTextRun, e os comentários de campo são explícitos de que esse ponto fica no espaço de texto, já dobrado através de Tm, Td, TD, e T*. O que ainda está faltando, e o que esses comentários dizem que quem chama precisa fornecer, é o CTM ativo naquela instrução exata — o produto de cada cm concatenado até então, aninhado dentro de quantos pares q/Q estejam abertos naquele ponto do stream

Diagrama do PDF Library for Delphi do pipeline de replay do fluxo de conteúdo: operadores PDF analisados alimentam o TPDFContentStateTracker um operador por vez, que mantém a CTM, a matriz de texto, os limites de recorte e a pilha q/Q como um record de snapshot
Cada operador interpretado atualiza o registro de estado gráfico do tracker, e um snapshot por instrução sai de uma única passada linear. O registro permanece utilizável sem que nenhum canvas de renderização esteja envolvido

Por que reproduzir um content stream em vez de renderizá-lo?

O PDF Library for Delphi mantém duas noções separadas de estado gráfico para dois trabalhos separados, e a divisão é deliberada. O registro de estado interno do renderizador carrega um handle de canvas de dispositivo vivo, um handle de região de recorte, e caches de rasterização de fonte — recursos reais atrelados a qualquer superfície atualmente sendo pintada, e sem sentido assim que essa superfície desaparece. TPDFContentGraphicsState não carrega nada disso: é um registro simples limitado aos valores que a ISO 32000-1 §8.4 define como alcançáveis apenas a partir de operadores de content stream — o CTM, estilo de linha, cor, estado de texto, e os limites de clip e path derivados. Como o registro não guarda nenhuma referência de canvas nem nenhum handle de arquivo aberto, quem chama pode analisar um content stream, percorrê-lo com TPDFContentStateTracker, e continuar usando os snapshots resultantes muito depois de o que quer que produziu os bytes ter desaparecido

Como o TPDFContentStateTracker constrói o CTM

TPDFContentStateTracker.Apply concatena os seis operandos de um operador cm no CTM do rastreador usando a mesma pré-multiplicação que o próprio PDF especifica: a nova matriz M2 se combina com o CTM atual como M2 × CTM, na convenção de vetor-linha onde um ponto se transforma como P′ = P × M (ISO 32000-1 §8.4). A parte fácil de errar fica no termo de translação, não na parte linear: a própria translação de M2 precisa passar pelo componente de rotação-e-escala do CTM atual antes de a translação do CTM atual ser adicionada por cima. Pule essa etapa e fixe à mão uma combinação ingênua componente a componente, em vez disso, e o primeiro cm isolado que você testar vai parecer correto enquanto cada coordenada rio abaixo de um segundo ou terceiro cm aninhado silenciosamente desvia, que é exatamente o tipo de bug que sobrevive à revisão de código porque o teste unitário que o capturaria precisa de pelo menos duas transformações encadeadas para falhar

var
  Prog: TPDFContentProgram;
  Runs: TPDFTextRunArray;
  States: TPDFContentGraphicsStateArray;
  DeviceX, DeviceY: Double;
  I: Integer;
begin
  Prog := TPDFContentProgram.Create;
  try
    Prog.Parse(ContentBytes);
    Runs := Prog.GetTextRuns;
    // Um snapshot antes da instrução por operador, calculado em uma única passada
    States := Prog.TraceGraphicsStates(nil, False);
    for I := 0 to High(Runs) do
    begin
      // OriginX/OriginY já incorporam Tm/Td/TD/T*; falta só a CTM ativa
      // nesta instrução (ISO 32000-1 8.4)
      with States[Runs[I].InstructionIndex].CTM do
      begin
        DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
        DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
      end;
      LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
    end;
  finally
    Prog.Free;
  end;
end;

O loop acima responde ao ponto de dor da abertura: TPDFContentProgram.GetTextRuns devolve OriginX e OriginY já dobrados através de Tm, Td, TD, e T*, e TraceGraphicsStates(nil, False) fornece a única peça restante, o CTM antes-da-instrução no índice exato em que cada run foi capturado, em uma única passada linear sobre todo o programa. Passar nil deixa o método possuir um rastreador privado para a chamada e liberá-lo internamente, o que é a escolha certa para uma varredura única; passar uma instância existente de TPDFContentStateTracker, em vez disso, é o que mantém o estado contínuo através de uma página montada a partir de mais de um content stream, já que a ISO 32000-1 trata o array /Contents de uma página como um único stream lógico e a pilha q/Q precisa concordar

A matriz de texto sobrevive ao Q; o estado gráfico não

A ISO 32000-1 §9.4.2 define Td, TD, Tm e T* como os operadores que constroem a matriz de texto e a matriz de linha de texto dentro de um bloco BT/ET, e o PDF Library for Delphi mantém essa distinção nítida: Td e TD concatenam uma translação pura na matriz de linha de texto, T* faz o mesmo usando o negativo da entrelinha atual, e só Tm substitui completamente ambas as matrizes pelos seis números que recebe. BT reinicia ambas as matrizes para a identidade, exatamente uma vez, no início do objeto de texto — mas q e Q não as tocam de forma alguma. TPDFContentStateTracker.Apply trata coRestoreState como caso especial precisamente por esse motivo: antes de desempilhar o estado salvo, ele captura a matriz de texto atual, a matriz de linha de texto, e a flag BT/ET, e as reaplica sobre o que quer que o estado desempilhado contivesse, porque um par q/Q envolvendo uma sequência de texto não deve mover a posição de texto para trás

Linha do tempo do PDF Library for Delphi do fluxo de exemplo BT, Td, q, cm, Tj, Q, Tj, ET com uma tabela de estados mostrando a escala da CTM desfeita por Q, enquanto o DX de 100 da matriz de texto sobrevive a ambas as execuções de glifos
O cm dentro do par q/Q dobra a CTM apenas para a primeira sequência de glifos, e Q remove essa escala novamente. A matriz de texto definida por Td sobrevive porque restaurar o estado a reaplica em vez de confiar no registro gráfico desempilhado
var
  Tracker: TPDFContentStateTracker;
  Prog: TPDFContentProgram;
  I: Integer;
begin
  Prog := TPDFContentProgram.Create;
  Tracker := TPDFContentStateTracker.Create;
  try
    Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
    for I := 0 to Prog.Count - 1 do
    begin
      Tracker.Apply(Prog[I]);
      if Prog[I].Op in [coShowText, coRestoreState] then
        LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
    end;
  finally
    Tracker.Free;
    Prog.Free;
  end;
end;

Rode essa sequência e o CTM reportado na segunda Tj está de volta à escala de identidade que tinha antes do q — o 2 0 0 2 0 0 cm dentro do par salvar/restaurar se foi, como q/Q exige. TextMatrix.DX naquela mesma instrução, porém, ainda é 100: o Td que o definiu rodou antes do q, então não é estado gráfico que o Q jamais teve autorização para tocar, e uma ferramenta que assumisse o contrário reportaria a segunda sequência de glifos começando na posição horizontal errada na página

O que acontece quando um operador de path de recorte roda?

Um operador W ou W* não encolhe o clip imediatamente; ele apenas registra qual regra de preenchimento usar, e a interseção de fato espera pelo operador de pintura de path que o segue, incluindo o pintor no-op n que autores de PDF rotineiramente usam exatamente para recortar sem desenhar nada. TPDFContentStateTracker espelha esse timing de duas etapas com precisão: coClip e coClipEvenOdd só definem uma flag pendente de regra de clip, e EndCurrentPath — invocado por todo operador de pintura de path — é o que de fato intersecta os limites do path pendente em ClipMinX, ClipMinY, ClipMaxX e ClipMaxY. Acertar esse estagiamento importa para o próprio contrato de snapshot antes/depois: um snapshot antes tomado exatamente na instrução W ainda precisa mostrar o clip antigo, mais largo, porque o clip ainda não entrou em vigor naquele ponto do stream, e colapsar as duas etapas em uma quebraria silenciosamente todo chamador que confia no estado-antes para significar o que diz

ClipBoundsExact diz a quem chama qual das duas situações está vendo, e só é True para um único retângulo alinhado aos eixos construído por re sobre um path por outro lado vazio — a única forma que o PDF Library for Delphi consegue representar exatamente como quatro números. Tudo mais — um retângulo rotacionado, um contorno curvo, um path composto com várias subpaths, ou um clip construído a partir de um modo de renderização de texto — ainda produz ClipMinX até ClipMaxY, mas com ClipBoundsExact limpo para False, um sinal honesto de que os quatro números são um limite externo seguro, e não a forma de clip verdadeira; chamadores que só precisam desse limite, como isolar uma sub-região retangular antes da down-conversão de meio-tom GDI descrita em renderizando páginas de PDF para monocromático de 1 bit, podem lê-lo diretamente, em vez de derivá-lo novamente da geometria da página

PDF Library for Delphi: linha do tempo de recorte em duas etapas em que W registra apenas a regra de recorte, o operador de pintura de caminho seguinte cruza com os limites pendentes, e ClipBoundsExact permanece verdadeiro apenas para um único retângulo re
O clip entra em vigor quando o próximo operador de pintura de path é executado, então um snapshot anterior capturado em W ainda reporta os limites antigos e mais amplos. Apenas um re alinhado aos eixos produz ClipBoundsExact true, enquanto todo o resto degrada honestamente para um limite externo seguro

Curvas Bézier: um limite exato ou um seguro

A forma mais barata de limitar um segmento cúbico de Bézier é pegar o casco convexo de seus quatro pontos de controle, e isso é sempre seguro porque a curva nunca sai dele — mas uma curva rasa e larga pode reportar uma caixa delimitadora muito maior do que a curva de fato ocupa, o que enfraquece a filtragem baseada em clip exatamente quando mais importa, em paths decorativos grandes. O PDF Library for Delphi resolve o problema mais estreito, em vez disso: para cada eixo, resolve a derivada da curva cúbica em busca de raízes dentro do intervalo aberto (0, 1) e avalia a curva em quaisquer raízes que encontre, junto com ambos os pontos finais, que é a forma fechada padrão de obter a extensão verdadeira alinhada aos eixos de uma curva, em vez de uma superestimativa. A precisão por curva, porém, não se transfere para o próprio clip: uma vez que um contorno curvo vira um path de recorte, ClipBoundsExact ainda cai para False para ele, porque uma caixa delimitadora, por mais estreita que seja, ainda não é a mesma forma que a curva que ela delimita, e o rastreador de estado prefere dizer isso a deixar quem chama assumir um retângulo onde de fato há uma curva

Lendo o estado antes e depois de cada operador

Se quem chama quer o estado antes ou depois depende inteiramente do que o operador faz: uma pergunta de desenho ou teste de acerto sobre um path ou sequência de texto quer o estado como estava no instante antes de esse operador rodar, já que isso é o que de fato determinou como o operador pintou, enquanto uma pergunta de diagnóstico sobre um operador que define estado, como gs, geralmente quer ver o que ele acabou de mudar. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) expõe exatamente essa escolha como um único booleano, calculando um TPDFContentGraphicsState por instrução em uma única passada linear sobre todo o programa, independentemente de qual instante é solicitado. GetGraphicsState(InstructionIndex, AfterInstruction, State) oferece a mesma escolha antes/depois para uma única instrução, em vez de todo o programa, mas ele reproduz a partir da instrução zero em toda chamada para chegar lá, de modo que varrer muitos índices chamando-o em loop custa O(n²) contra uma única chamada O(n) a TraceGraphicsStates sobre o mesmo programa

var
  Before, After: TPDFContentGraphicsState;
begin
  // Mesmo índice de instrução, dois instantes: antes e depois de ela rodar
  Prog.GetGraphicsState(CmIndex, False, Before);
  Prog.GetGraphicsState(CmIndex, True, After);
  // Before.CTM reflete todos os cm anteriores; After.CTM já incorpora
  // também a concatenação da própria instrução
end;

Convivendo com content streams malformados

Dois tipos de entrada malformada são comuns o bastante em produtores de PDF reais para que TPDFContentStateTracker precise tolerá-los, em vez de falhar neles. O primeiro é um path que atravessa uma fronteira q/Q: o path atual, o ponto atual, e a contagem de subpaths não são parâmetros de estado gráfico — a ISO 32000-1 §8.4 cobre o que q e Q salvam e restauram, e o path atual em construção não está entre eles — de modo que TPDFContentStateTracker rastreia esse dado inteiramente fora do estado salvo, e uma subpath iniciada antes de um q ainda está lá, não pintada, imediatamente depois do Q correspondente. O segundo é um Q solitário sem nenhum q correspondente em nenhum lugar antes dele no stream, não raro em saída de geradores que montam fragmentos de content stream por concatenação e erram a contabilidade. TPDFContentStateTracker.RestoreUnderflowCount conta cada um desses eventos, em vez de levantar uma exceção ou corromper o estado: um Q sem par simplesmente deixa o estado gráfico atual exatamente como estava, como se aquela instrução tivesse sido um no-op, de modo que o resto do stream continua se reproduzindo sobre um estado são e quem chama ainda pode decidir depois, a partir da contagem, se a entrada vale a pena sinalizar de volta a quem a produziu

A composição de CTM, a independência da matriz de texto em relação a q/Q, e a realização em etapas de um path de recorte não dependem de como ou se o content stream chega a ser pintado, que é exatamente o ponto: o mesmo snapshot de TPDFContentStateTracker é correto seja a página nunca renderizada de forma alguma, seja prestes a ser entregue a qualquer back end que o PDF Library for Delphi selecione para aquele arquivo, incluindo a troca de motor em tempo de execução coberta em o guia de renderização multi-motor de PDF no PDF Library for Delphi. Análise de conteúdo, mapeamento de coordenadas, e ferramentas de redação podem todas rodar inteiramente sobre a saída do rastreador, muito antes de, ou completamente sem, jamais pedir a um renderizador para se envolver

A reprodução de content stream por meio de TPDFContentStateTracker faz parte do framework de edição de conteúdo estruturado embutido no PDF Library for Delphi, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder