Uma página PDF não armazena píxeis nem armazena uma árvore de objetos geométricos da forma que o SVG faz. Armazena um programa. Cada linha, curva, preenchimento e imagem posicionada na página é o resultado da execução de uma sequência de operadores num fluxo de conteúdo (content stream), de cima para baixo, face a um estado gráfico em execução. Compreenda este facto e a maior parte do comportamento do formato deixará de ser surpreendente: a razão pela qual um preenchimento necessita de um operador de pintura separado após a construção do caminho, por que razão as cores e larguras de linha transitam de uma forma para a seguinte a menos que as delimite, e porque é que o mesmo código de desenho pode terminar em locais totalmente diferentes após uma única transformação de coordenadas. Esta é uma visita guiada a esse modelo de execução conforme definido na norma ISO 32000: os operadores com que se depara ao abrir um fluxo de conteúdo e as regras que determinam o que é apresentado na página.
O fluxo de conteúdo é bytecode pós-fixado
Um fluxo de conteúdo é uma sequência linear de bytes composta por operandos seguidos de operadores. Os operandos surgem primeiro e o operador que os consome surge no fim, o que representa o inverso de uma chamada de função e é idêntico a uma máquina de pilha: empilham-se os números e depois executa-se o verbo. Não existe aninhamento, sintaxe de expressões ou variáveis. O contorno de um triângulo resume-se a cinco linhas disto:
100 100 m % moveto: start a new subpath at (100, 100)
200 200 l % lineto: add a segment to (200, 200)
300 100 l % lineto: add a segment to (300, 100)
h % closepath: connect back to the start
S % stroke: paint the path outline
Os operadores são intencionalmente concisos. Uma página real contém milhares destes operadores, normalmente comprimidos com FlateDecode. O custo dessa compactação é que o fluxo não contém qualquer estrutura passível de consulta: um leitor não pode perguntar "onde está o título nesta página", limitando-se a executar o programa para ver que tinta é depositada e em que local. Essa é a razão fundamental pela qual a extração de texto a partir de ficheiros PDF arbitrários é complexa.
A origem situa-se no canto inferior esquerdo e o Y cresce para cima
Antes de qualquer coordenada fazer sentido, é necessário saber onde se situa o ponto (0, 0). O PDF define a origem no canto inferior esquerdo da página, com o X a aumentar para a direita e o Y a aumentar para cima, medidos em pontos (à razão de 72 pontos por polegada, conforme ISO 32000-2 §8.3.2). Numa página de formato Letter (EUA), o limite superior situa-se em y = 792 e não em y = 0. Quem provém da área de gráficos de ecrã, onde a origem é no canto superior esquerdo e o Y cresce para baixo, erra nesta lógica à primeira tentativa, desenhando a primeira linha fora do limite inferior da página. A unidade é igualmente independente do suporte físico: 72 unidades equivalem a uma polegada, quer a página seja renderizada num ecrã de telemóvel ou numa filmadora de chapa (imagesetter).
A maioria das bibliotecas de desenho de páginas herda diretamente esta convenção. No HotPDF, por exemplo, o método TextOut e as chamadas de caminho medem todos a partir do canto inferior esquerdo em pontos, pelo que um valor próximo da altura da página posiciona o conteúdo no topo:
// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // emits the moveto/lineto/stroke operators
Essa sequência de chamadas compila-se exatamente nos operadores m, l e S detalhados acima. A biblioteca limita-se a escrever no fluxo de conteúdo, e saber o que ela emite é o que lhe permite compreender o resultado caso uma forma geométrica termine num local inesperado.
Construir o caminho e depois pintá-lo
O PDF separa a construção do caminho da sua pintura, e esta divisão não é mero formalismo. Primeiro descreve uma forma geométrica com operadores de construção que não acrescentam nada visível e, em seguida, executa um único operador de pintura que decide o que fazer com o caminho acumulado. O mesmo triângulo pode traduzir-se num contorno, num preenchimento sólido ou em ambos, dependendo apenas do verbo com que finaliza.
Os operadores de construção são poucos. O m inicia um novo subcaminho num ponto. O l adiciona um segmento retilíneo. O c adiciona uma curva de Bezier cúbica a partir de seis operandos (dois pontos de controlo e um ponto final). O re é un atalho que adiciona um retângulo completo a partir de quatro valores (x, y, largura, altura). O h fecha o subcaminho atual de volta ao seu início. Nenhum deles deposita tinta na página; limitam-se a acumular geometria.
200 250 m % start the subpath
300 350 400 450 500 250 c % cubic Bezier: two control points, then endpoint
150 200 re % a 150 x 200 rectangle, added as its own subpath
h % close
O exemplo original utilizava a variante y (atualmente obsoleta) do operador de curva; o c com os seus três pontos explícitos é a forma que encontrará na prática e a que deve utilizar. Uma vez existente o caminho, um operador de pintura finaliza-o. O vocabulário é reduzido e vale a pena memorizá-lo, pois cada forma geométrica em cada página termina com um destes operadores:
Sdesenha o contorno do caminho utilizando a largura de linha e a cor de traço atuais.fpreenche o interior utilizando a cor de preenchimento atual e a regra de enrolamento diferente de zero (nonzero winding rule).f*preenche utilizando a regra par-ímpar (even-odd rule), relevante para formas com autointersecção e formas com orifícios.Bpreenche e depois desenha o contorno numa única operação; obfecha o caminho primeiro.nnão pinta nada, que é a forma como um caminho se converte numa região de recorte (clip region) sem deixar qualquer marca visível.
A regra de enrolamento é o aspeto que gera mais erros. O preenchimento diferente de zero (nonzero winding rule: f, B) contabiliza os cruzamentos sinalizados de um raio a partir do ponto de teste e preenche sempre que a contagem não for zero, pelo que um orifício só permanece vazio se o seu subcaminho rodar no sentido oposto ao do caminho externo. A regra par-ímpar (even-odd rule: f*, B*) alterna a cada cruzamento, independentemente da direção. Se uma forma de "rosca" (donut) surgir totalmente sólida, significa que o círculo interno roda no mesmo sentido do círculo externo, necessitando de o inverter ou de mudar para a regra par-ímpar.
A cor é um modo, não um parâmetro
A cor num fluxo de conteúdo é persistente. Ao definir uma cor, esta permanece ativa até que defina outra ou restaure um estado anterior, razão pela qual uma alteração de cor não delimitada pinta silenciosamente tudo o que for desenhado a seguir. O PDF mantém ainda a cor de preenchimento e a cor de traço como duas definições independentes, utilizando operadores em minúsculas para o preenchimento e maiúsculas para o traço. Os espaços de cor do dispositivo (device color spaces) têm os seus próprios atalhos:
0.5 g % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg % DeviceRGB fill
0.8 0.2 0.1 RG % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k % DeviceCMYK fill
O DeviceRGB adequa-se à visualização em ecrã, o DeviceCMYK é o esperado na produção de impressão e o DeviceGray constitui a opção mais económica para conteúdos monocromáticos. Os espaços do dispositivo são práticos, mas não estão calibrados: o mesmo trio RGB pode renderizar de forma diferente em dois ecrãs distintos, sendo este o problema que os espaços de cor baseados em perfis ICC e as intenções de saída PDF/A procuram resolver. Para trabalhos onde a cor seja crítica, seleciona-se um espaço calibrado com cs e CS e definem-se os componentes com sc e scn, mas para documentos comuns os atalhos do dispositivo cumprem bem a sua função. Uma biblioteca encapsula estas definições em chamadas tipadas. O HotPDF, por exemplo, aceita uma única TColor e emite os operadores correspondentes:
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, width, height
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, radius
Pdf.CurrentPage.Fill;
O estado gráfico e o stack q/Q
Tudo o que não pertence ao próprio caminho reside no estado gráfico: matriz de transformação atual, cores de preenchimento e traço, largura de linha, padrão de tracejado, região de recorte e canal alfa. O estado é global e mutável, pelo que a única forma segura de realizar uma alteração local consiste em guardar todo o estado, modificá-lo, desenhar e depois revertê-lo. É essa a função do q e do Q. O q empilha uma cópia do estado gráfico atual; o Q retira-a do stack, descartando todas as alterações efetuadas desde o q correspondente.
q % save the entire graphics state
2 0 0 2 100 100 cm % concatenate a transform: scale 2x, translate to (100,100)
0.8 g % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q % restore: transform and color revert
Elementos q e Q não balanceados constituem uma causa frequente de anomalias em fluxos de conteúdo criados manualmente ou unidos. Um q perdido sem o Q correspondente deixa a pilha com elementos pendentes quando a página termina; um Q adicional causa um sobredesvio negativo (underflow) na pilha. Em qualquer dos casos, o leitor pode manter ativo um recorte ou uma transformação antiga, fazendo com que o conteúdo desapareça ou termine no local errado. Se os elementos gráficos desaparecerem sem qualquer motivo explicável pelo caminho, verifique primeiro o estado da pilha.
A CTM transforma cada coordenada
A matriz de transformação atual (CTM - Current Transformation Matrix) interpõe-se entre os números dos seus operadores e a página física. Cada coordenada é multiplicada pela CTM antes de qualquer desenho ser efetuado, pelo que alterar a matriz modifica o local e a forma como todo o desenho subsequente surge, sem necessidade de alterar uma única coordenada do caminho. O operador cm concatena uma nova matriz à atual, aceitando seis operandos que correspondem à matriz afim [a b c d e f]:
1 0 0 1 100 50 cm % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm % rotate 45 degrees (cos/sin in a, b, c, d)
Existem dois detalhes que costumam causar problemas. Em primeiro lugar, o cm realiza uma composição e não uma substituição, o que significa que as transformações se acumulam e a ordem importa: aplicar escala e depois translação é diferente de aplicar translação e depois escala. Em segundo lugar, a rotação e a escala têm como pivot a origem atual e não o centro da sua figura geométrica. Para rodar um elemento no próprio local, é necessário transladá-lo para a origem, aplicar a rotação e efetuar a translação inversa, tudo isto delimitado por q/Q. Esta mesma matriz é a responsável pelo posicionamento de imagens, o último elemento a analisar.
As imagens e o conteúdo reutilizável são XObjects
As imagens matriciais não se encontram em linha no fluxo de conteúdo. São guardadas como XObjects de imagem, que são objetos externos com o seu próprio dicionário que descreve a largura, altura, profundidade de bits, espaço de cor e filtro de compressão, limitando-se o fluxo de conteúdo a referenciá-los. Uma fotografia em formato JPEG declara-se da seguinte forma:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % the image data is a JPEG stream
>>
Um XObject de imagem desenha-se no quadrado unitário: ocupa sempre a região de (0, 0) a (1, 1) no espaço do utilizador. Não se especifica uma posição ou dimensão diretamente na imagem. Em vez disso, configura-se a CTM para que esse quadrado unitário se mapeie no retângulo pretendido e, em seguida, invoca-se a imagem com o operador Do. É por isso que posicionar uma imagem se traduz sempre numa transformação seguida de uma invocação, envolvidas num save/restore (q/Q) para que a escala não se propague à operação seguinte:
q
640 0 0 480 50 300 cm % map the unit square to a 640x480 box at (50, 300)
/Photo Do % paint the image XObject
Q
O mesmo mecanismo Do serve de base aos XObjects de formulário (form XObjects), que contêm elementos gráficos reutilizáveis, como um logótipo ou um carimbo repetido, sob a forma de um fluxo de conteúdo próprio com uma caixa de delimitação (bounding box). Define-se uma vez, invoca-se várias vezes com CTMs distintas e os bytes surgem no ficheiro uma única vez. A maioria das bibliotecas oculta este processo atrás de uma única chamada de posicionamento: o HotPDF regista um mapa de bits com AddImage e posiciona-o com ShowImage, aceitando valores explícitos de x, y, largura e altura em vez de exigir a construção manual da matriz:
var
Bmp: TBitmap;
ImgIndex: Integer;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromFile('logo.bmp');
ImgIndex := Pdf.AddImage(Bmp, icFlate);
// x, y (bottom-left), width, height, rotation angle
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Sob essa única linha de código, a biblioteca escreve o dicionário do XObject de imagem, configura a CTM para dimensionar e posicionar o quadrado unitário e emite o operador Do. Conhecer o modelo subjacente é fundamental porque explica qualquer resultado anómalo: uma imagem distorcida deve-se a uma CTM com fatores de escala incompatíveis, um logótipo idêntico em quarenta páginas representa um único XObject de formulário invocado quarenta vezes, e uma imagem renderizada invertida indica uma inversão de sinal na matriz, e não um ficheiro corrompido.
Onde isto nos leva
O modelo gráfico é simples depois de compreendermos o seu esquema. Um fluxo de conteúdo é bytecode pós-fixado executado contra um estado mutável; as coordenadas começam no canto inferior esquerdo e passam pela CTM; os caminhos são construídos em silêncio e pintados com um operador explícito; as configurações de cor e linha persistem a menos que as delimite com q/Q; as imagens e os elementos gráficos reutilizáveis são XObjects posicionados através da transformação de um quadrado unitário. Praticamente todos os resultados de renderização confusos reduzem-se a uma destas cinco regras. Se pretender ver como estes operadores gráficos se posicionam no modelo de objetos mais amplo, consulte a visão geral técnica da estrutura de ficheiros PDF que descreve essa camada, e a demonstração de construção de um PDF simples a partir do zero que percorre os bytes de ponta a ponta. O desenho de texto reside na sua própria família de operadores e apresenta as suas próprias dificuldades, abordadas no artigo complementar sobre processamento de texto e tipos de letra em PDF.
As chamadas de desenho em Delphi aqui demonstradas (MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage e ShowImage) fazem parte do HotPDF Component para Delphi e C++Builder, que emite estes operadores de fluxo de conteúdo por si.