O PDFlibPas, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder, reproduz o fluxo de conteúdo de uma página através da sua classe TPDFContentStateTracker sem sequer tocar numa tela de renderização. Alimentar o rastreador com um operador interpretado de cada vez mantém um registo contínuo do estado gráfico — matriz de transformação atual, matriz de texto, limites de recorte, e a pilha de guardar q/Q — disponível para captura antes ou depois de cada operador ser executado
Perguntar onde é que uma sequência de texto efetivamente aterra na página impressa, e os números em bruto do fluxo de conteúdo por si só vão sempre induzir em erro. O TPDFContentProgram.GetTextRuns já reporta o ponto de ancoragem de cada instrução de apresentação de texto através dos campos OriginX e OriginY em TPDFTextRun, e os comentários de campo são explícitos ao afirmar que esse ponto está no espaço de texto, já dobrado através de Tm, Td, TD, e T*. O que ainda falta, e o que esses comentários dizem que um chamador tem de fornecer, é a CTM ativa nessa instrução exata — o produto de cada cm concatenado até esse ponto, aninhado dentro de quantos pares q/Q estiverem abertos nesse ponto do fluxo
Porque reproduzir um fluxo de conteúdo em vez de o renderizar?
O PDFlibPas mantém duas noções separadas de estado gráfico para duas tarefas separadas, e a separação é deliberada. O registo de estado interno do renderizador transporta um handle de tela de dispositivo ativo, um handle de região de recorte, e caches de rasterização de tipo de letra — recursos reais ligados a qualquer que seja a superfície atualmente a ser pintada, e sem significado assim que essa superfície deixa de existir. O TPDFContentGraphicsState não transporta nada disso: é um simples registo limitado aos valores que a ISO 32000-1 §8.4 define como alcançáveis apenas a partir de operadores de fluxo de conteúdo — a CTM, o estilo de linha, a cor, o estado de texto, e os limites derivados de recorte e de percurso. Como o registo não contém qualquer referência de tela nem qualquer handle de ficheiro aberto, um chamador pode interpretar um fluxo de conteúdo, percorrê-lo com TPDFContentStateTracker, e continuar a usar as capturas resultantes muito depois de o que quer que tenha produzido os bytes já ter deixado de existir
Como o TPDFContentStateTracker constrói a CTM
O TPDFContentStateTracker.Apply concatena os seis operandos de um operador cm na CTM do rastreador usando a mesma pré-multiplicação que o próprio PDF especifica: a nova matriz M2 combina-se com a CTM atual como M2 × CTM, na convenção de vetor-linha em que um ponto se transforma como P′ = P × M (ISO 32000-1 §8.4). A parte fácil de fazer mal está no termo de translação, não na parte linear: a própria translação de M2 tem de passar pela componente de rotação-e-escala da CTM atual antes de a translação da CTM atual ser somada por cima. Se se saltar esse passo e se codificar em vez disso uma combinação ingénua componente a componente, o primeiro cm isolado que se testar vai parecer correto enquanto cada coordenada a jusante de um segundo ou terceiro cm aninhado vai derivar silenciosamente, exatamente o tipo de erro que sobrevive a uma revisão de código porque o teste unitário que o apanharia 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 ciclo acima responde ao ponto de dor do início: o TPDFContentProgram.GetTextRuns devolve OriginX e OriginY já dobrados através de Tm, Td, TD, e T*, e o TraceGraphicsStates(nil, False) fornece a única peça em falta, a CTM antes da instrução no índice exato em que cada sequência foi capturada, numa única passagem linear sobre todo o programa. Passar nil deixa o método possuir um rastreador privado para a chamada e libertá-lo internamente, a escolha certa para uma verificação pontual; passar em vez disso uma instância existente de TPDFContentStateTracker é o que mantém o estado contínuo através de uma página montada a partir de mais de um fluxo de conteúdo, já que a ISO 32000-1 trata o array /Contents de uma página como um único fluxo lógico e a pilha q/Q tem de concordar com isso
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ó o Tm substitui completamente ambas as matrizes pelos seis números que lhe são dados. O BT repõe ambas as matrizes na identidade, exatamente uma vez, no início do objeto de texto — mas o q e o Q não lhes tocam de todo. O TPDFContentStateTracker.Apply trata coRestoreState como um caso especial precisamente por esta razão: antes de retirar o estado guardado da pilha, captura a matriz de texto atual, a matriz de linha de texto, e a flag BT/ET, e volta a aplicá-las por cima do que quer que o estado retirado contivesse, porque não é suposto um par q/Q envolvendo uma sequência de texto recuar a posição do texto
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;
Ao correr esta sequência, a CTM reportada no segundo Tj volta à escala de identidade que tinha antes do q — o 2 0 0 2 0 0 cm dentro do par de guardar/restaurar desapareceu, como o q/Q exige. O TextMatrix.DX nessa mesma instrução, contudo, continua a ser 100: o Td que o definiu correu antes do q, pelo que não é estado gráfico que o Q alguma vez estivesse autorizado a tocar, e uma ferramenta que presumisse o contrário reportaria a segunda sequência de glifos a começar na posição horizontal errada da página
O que acontece quando corre um operador de percurso de recorte?
Um operador W ou W* não reduz o recorte de imediato; apenas regista qual a regra de preenchimento a usar, e a interseção real espera pelo operador de pintura de percurso que se segue, incluindo o pintor sem operação n que os autores de PDF usam rotineiramente precisamente para recortar sem desenhar nada. O TPDFContentStateTracker espelha esse tempo de dois passos com precisão: coClip e coClipEvenOdd apenas definem uma flag de regra de recorte pendente, e EndCurrentPath — invocado por cada operador de pintura de percurso — é o que efetivamente interseta os limites do percurso pendente em ClipMinX, ClipMinY, ClipMaxX, e ClipMaxY. Acertar esta faseação importa para o próprio contrato de captura antes/depois: uma captura anterior tirada exatamente na instrução W ainda tem de mostrar o recorte antigo, mais largo, porque o recorte ainda não fez efeito nesse ponto do fluxo, e colapsar os dois passos num só quebraria silenciosamente qualquer chamador que confie em que o estado anterior signifique o que diz
O ClipBoundsExact diz a um chamador qual das duas situações está a ver, e só é True para um único retângulo alinhado com os eixos construído por re sobre um percurso de outro modo vazio — a única forma que o PDFlibPas consegue representar exatamente como quatro números. Tudo o resto — um retângulo rodado, um contorno curvo, um percurso composto com vários subpercursos, ou um recorte construído a partir de um modo de renderização de texto — continua a produzir ClipMinX a ClipMaxY, mas com ClipBoundsExact definido como False, um sinal honesto de que os quatro números são um limite exterior seguro e não a verdadeira forma de recorte; chamadores que só precisem desse limite, como isolar uma sub-região retangular antes da conversão para meios-tons GDI descrita em renderizar páginas de PDF para monocromático de 1 bit, podem lê-lo diretamente em vez de o rederivar a partir da geometria da página
Curvas de Bézier: um limite exato ou um limite seguro
A forma mais barata de delimitar um segmento cúbico de Bézier é tomar o invólucro convexo dos seus quatro pontos de controlo, e isso é sempre seguro porque a curva nunca o abandona — mas uma curva rasa e larga pode reportar uma caixa delimitadora muito maior do que a curva efetivamente ocupa, o que enfraquece a filtragem baseada em recorte precisamente quando mais importa, em grandes percursos decorativos. O PDFlibPas resolve antes o problema mais rigoroso: para cada eixo, resolve a derivada da curva cúbica para raízes dentro do intervalo aberto (0, 1) e avalia a curva em qualquer raiz que encontre, juntamente com ambos os extremos, a forma padrão de forma fechada para obter a verdadeira extensão alinhada com os eixos de uma curva, em vez de uma sobrestimativa. A precisão por curva, contudo, não se transmite ao próprio recorte: assim que um contorno curvo se torna um percurso de recorte, ClipBoundsExact continua a passar para False, porque uma caixa delimitadora, por mais rigorosa que seja, continua a não ser a mesma forma que a curva que delimita, e o rastreador de estado prefere dizê-lo a deixar um chamador presumir que existe um retângulo onde na realidade está uma curva
Ler o estado antes e depois de cada operador
Se um chamador quer o estado anterior ou o posterior depende inteiramente do que o operador faz: uma questão de desenho ou de deteção de acerto sobre um percurso ou uma sequência de texto quer o estado tal como estava no instante imediatamente antes de esse operador correr, já que foi isso que efetivamente determinou como o operador pintou, enquanto uma questão de diagnóstico sobre um operador de definição de estado como gs normalmente quer ver o que este acabou de alterar. O TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) expõe exatamente essa escolha como um único booleano, calculando um TPDFContentGraphicsState por instrução numa única passagem linear sobre todo o programa, independentemente de qual instante seja pedido. O GetGraphicsState(InstructionIndex, AfterInstruction, State) oferece a mesma escolha antes/depois para uma única instrução em vez de todo o programa, mas reproduz a partir da instrução zero em cada chamada para lá chegar, pelo que percorrer muitos índices chamando-o num ciclo 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;
Conviver com fluxos de conteúdo malformados
Dois tipos de entrada malformada são suficientemente comuns em produtores reais de PDF para que o TPDFContentStateTracker tenha de os tolerar em vez de falhar sobre eles. O primeiro é um percurso que atravessa uma fronteira q/Q: o percurso atual, o ponto atual, e a contagem de subpercursos não são parâmetros de estado gráfico — a ISO 32000-1 §8.4 cobre o que q e Q guardam e restauram, e o percurso atual em construção não está incluído — pelo que o TPDFContentStateTracker rastreia esses dados inteiramente fora do estado guardado, e um subpercurso iniciado antes de um q continua lá, por pintar, imediatamente depois do Q correspondente. O segundo é um Q solitário sem nenhum q correspondente antes dele no fluxo, não raro na saída de geradores que montam fragmentos de fluxo de conteúdo por concatenação e erram a contabilidade. O TPDFContentStateTracker.RestoreUnderflowCount conta cada um desses eventos em vez de gerar uma exceção ou corromper o estado: um Q sem par deixa simplesmente o estado gráfico atual exatamente como estava, como se essa instrução tivesse sido uma não-operação, pelo que o resto do fluxo continua a reproduzir sobre um estado são e um chamador ainda pode decidir depois, a partir da contagem, se vale a pena assinalar a entrada de volta a quem a produziu
A composição da CTM, a independência da matriz de texto em relação ao q/Q, e a concretização faseada de um percurso de recorte não dependem de como, ou se, o fluxo de conteúdo alguma vez chega a ser pintado, o que é precisamente o ponto: a mesma captura do TPDFContentStateTracker é correta quer a página nunca seja renderizada de todo, quer esteja prestes a ser entregue a qualquer que seja o back end que o PDFlibPas selecione para esse ficheiro, incluindo a troca de motor em tempo de execução abordada em o guia sobre renderização de PDF com múltiplos motores no PDFlibPas. A análise de conteúdo, o mapeamento de coordenadas, e as ferramentas de redação podem todos correr inteiramente sobre a saída do rastreador, muito antes de, ou sem alguma vez, pedir a um renderizador que intervenha
A reprodução de fluxo de conteúdo através de TPDFContentStateTracker faz parte da estrutura de edição de conteúdo estruturado incorporada no PDFlibPas, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder