O PDFlibPas, 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
Por que reproduzir um content stream em vez de renderizá-lo?
O PDFlibPas 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;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (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 PDFlibPas 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
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 PDFlibPas 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
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 PDFlibPas 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
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
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 PDFlibPas 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 PDFlibPas. 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 PDFlibPas, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder