Uma página PDF não armazena pixels e não armazena uma árvore de objetos de forma da maneira que o SVG faz. Ela armazena um programa. Cada linha, curva, preenchimento e imagem colocada na página é o resultado da execução de uma sequência de operadores em um fluxo de conteúdo, de cima para baixo, contra um estado gráfico em execução. Entenda esse fato único e a maior parte do comportamento do formato deixa de ser surpreendente: por que um preenchimento precisa de um operador de pintura separado depois que o caminho é construído, por que cores e larguras de linha vazam de uma forma para a próxima, a menos que você as agrupe (bracket), por que o mesmo código de desenho pode parar em lugares completamente diferentes após uma única transformação de coordenadas. Este é um tour desse modelo de execução conforme definido no ISO 32000: os operadores que você encontra ao abrir um fluxo de conteúdo e as regras que decidem o que aparece na página
O fluxo de conteúdo é bytecode pós-fixado
Um fluxo de conteúdo é uma sequência plana de bytes de operandos seguidos por operadores. Os operandos vêm primeiro, o operador que os consome vem por último, o que é o inverso de uma chamada de função e idêntico a uma máquina de pilha (stack machine): empurre os números e depois emita o verbo. Não há aninhamento, sintaxe de expressão, não há variáveis. Um contorno de triângulo são cinco linhas disso:
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 concisos de propósito. Uma página real tem milhares deles, geralmente compactados com FlateDecode. O custo dessa compactação é que o fluxo não carrega estrutura que você possa consultar: um visualizador não pode perguntar "onde está o título nesta página", ele só pode executar o programa e ver onde a tinta cai. Essa é a razão fundamental pela qual a extração de texto de PDFs arbitrários é difícil
A origem é inferior esquerda e Y cresce para cima
Antes que qualquer coordenada faça sentido, você precisa saber onde fica (0, 0). O PDF coloca a origem no canto inferior esquerdo da página, com X aumentando para a direita e Y aumentando para cima, medidos em pontos (72 pontos por polegada, de acordo com ISO 32000-2 §8.3.2). Em uma página Carta (US Letter), a borda superior fica em y = 792, não em y = 0. Qualquer um que venha de gráficos de tela, onde a origem é no canto superior esquerdo e o Y cresce para baixo, confunde isso na primeira tentativa e desenha a primeira linha para fora da parte inferior da página. A unidade também é independente da mídia: 72 unidades equivalem a uma polegada, não importa se a página é renderizada na tela de um celular ou em uma imagesetter
A maioria das bibliotecas de desenho de páginas herda essa convenção diretamente. No HotPDF, por exemplo, o TextOut e as chamadas de caminho medem todos do canto inferior esquerdo em pontos, portanto, um valor próximo da altura da página coloca 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 exatamente para os operadores m, l e S acima. A biblioteca é um datilógrafo para o fluxo de conteúdo, nada mais, e saber o que ela emite é o que permite raciocinar sobre a saída quando uma forma pousa em algum lugar que você não esperava
Construa o caminho, depois pinte-o
O PDF separa a construção do caminho da pintura do caminho, e a separação não é pedantismo. Primeiro você descreve uma forma com operadores de construção que não adicionam nada visível e, em seguida, emite um único operador de pintura que decide o que fazer com o caminho acumulado. O mesmo triângulo pode ser um contorno, um preenchimento sólido, ou ambos, dependendo apenas do verbo com o qual você termina
Os operadores de construção são poucos. m inicia um novo subcaminho (subpath) em um ponto. l adiciona um segmento reto. c adiciona uma curva Bezier cúbica a partir de seis operandos, dois pontos de controle e um ponto de extremidade. re é um atalho que adiciona um retângulo inteiro a partir de um quádruplo x, y, largura, altura. h fecha o subcaminho atual de volta ao seu início. Nenhum deles coloca tinta na página; eles apenas acumulam 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 usou a variante y agora obsoleta do operador de curva; c com seus três pontos explícitos é a forma que você verá na prática e a qual deve buscar. Uma vez que o caminho existe, um operador de pintura o finaliza. O vocabulário é pequeno e vale a pena memorizar, porque cada forma em cada página termina com um destes:
Straça o contorno do caminho usando a largura da linha atual e a cor do traço (stroke color)fpreenche o interior usando a cor de preenchimento atual e a regra de preenchimento "nonzero winding" (nonzero winding rule)f*preenche usando a regra par-ímpar (even-odd rule), que é importante para formas com autointerseção e formas com furosBpreenche e, em seguida, traça o contorno em uma única operação;bfecha o caminho primeironpinta nada, o que é como um caminho se torna uma região de corte (clip region) sem deixar uma marca visível
A regra de preenchimento (winding rule) é a parte em que as pessoas erram. A regra 'nonzero' (f, B) conta os cruzamentos assinados de um raio do ponto de teste e preenche onde a contagem não é zero, então um buraco só fica vazio se seu subcaminho tiver a direção oposta à do externo. O par-ímpar (f*, B*) alterna (toggles) em cada cruzamento independentemente da direção. Se uma forma de "donut" (rosca) sair sólida, o círculo interno está desenhado na mesma direção que o externo, e você precisa revertê-lo ou alternar para a regra par-ímpar
Cor é um modo, não um parâmetro
A cor em um fluxo de conteúdo é pegajosa (sticky). Você define uma cor e ela permanece definida até que você defina outra ou restaure um estado anterior, o que explica por que uma alteração de cor não agrupada (unbracketed) silenciosamente tinge tudo o que é desenhado depois dela. O PDF também mantém a cor de preenchimento e a cor do traço como duas configurações independentes, com operadores minúsculos para preenchimento e maiúsculos para o traço. Os espaços de cor do dispositivo têm, cada um, sua própria forma abreviada:
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 atende à saída de tela, o DeviceCMYK é o que a produção de impressão espera, e o DeviceGray é a escolha mais compacta para conteúdo monocromático. Os espaços do dispositivo são convenientes, mas não calibrados: a mesma tripla RGB pode ser renderizada de maneira diferente em dois monitores, que é o problema que os espaços de cores baseados em ICC e as intenções de saída PDF/A existem para resolver. Para trabalho crítico com cores, você seleciona um espaço calibrado com cs e CS e define os componentes com sc e scn, mas, para documentos comuns, as formas abreviadas do dispositivo resolvem o problema. Uma biblioteca agrupa isso em chamadas tipadas. O HotPDF, por exemplo, pega um único 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 a pilha q/Q
Tudo o que não é o próprio caminho vive no estado gráfico: matriz de transformação atual, cores de preenchimento e de traço, largura de linha, padrão de tracejado, região de corte, alfa. O estado é global e mutável, então a única maneira segura de fazer uma alteração local é salvar tudo, modificá-lo, desenhar e revertê-lo. Isso é o que o q e Q fazem. O q empurra (pushes) uma cópia do estado atual para uma pilha; Q retira (pops), descartando todas as alterações feitas 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
Os q e Q não balanceados são uma forma comum pela qual um fluxo de conteúdo montado ou costurado à mão dá errado. Um q solto sem o Q correspondente deixa a pilha profunda quando a página termina; um Q extra esvazia (underflows) a pilha. De qualquer forma, um visualizador pode manter um recorte (clip) ou transformação antigos em vigor, e o conteúdo desaparece ou vai parar no lugar errado. Quando os gráficos desaparecerem sem nenhuma razão que o caminho possa explicar, verifique (audit) a pilha de estados primeiro
O CTM transforma todas as coordenadas
A matriz de transformação atual (CTM) fica entre os números nos seus operadores e a página real. Cada coordenada é multiplicada pela CTM antes que algo seja desenhado, portanto, alterar a matriz muda onde e como todo desenho subsequente aparece sem tocar numa única coordenada de caminho. O operador cm concatena uma nova matriz na matriz atual, aceitando seis operandos que são mapeados para a 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)
Duas coisas confundem as pessoas. Primeiro, o cm compõe em vez de substituir, de modo que as transformações se acumulam e a ordem importa: dimensionar e depois transladar não é o mesmo que transladar e depois dimensionar. Em segundo lugar, a rotação e a escala giram em torno da origem atual, não do centro da sua forma; portanto, para rotacionar algo no mesmo local, você o translada para a origem, rotaciona, e em seguida, o translada de volta, tudo envolto em q/Q. Esta mesma matriz é o que posiciona as imagens, a última parte que vale a pena ver
Imagens e conteúdo reutilizável são XObjects
As imagens raster não vivem na mesma linha (inline) no fluxo de conteúdo. Elas são armazenadas como XObjects de imagem, objetos externos com seu próprio dicionário descrevendo largura, altura, profundidade de bits, espaço de cor e filtro de compactação, e o fluxo de conteúdo apenas as referencia. Uma foto no formato JPEG é declarada assim:
/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 no quadrado unitário: ele sempre ocupa a região de (0, 0) a (1, 1) no espaço do usuário. Você não passa a ele uma posição ou tamanho. Em vez disso, você configura o CTM para que o quadrado unitário seja mapeado para o retângulo desejado, então você o invoca com o Do. É por isso que colocar uma imagem é sempre uma transformação seguida por uma invocação, envolta em um salvar/restaurar para que a escala não vaze (bleed) para a próxima operação:
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 orienta os XObjects de formato (form XObjects), que guardam um pedaço reutilizável de gráficos, um logotipo ou um carimbo repetido, como o seu próprio fluxo de conteúdo com uma caixa delimitadora (bounding box). Defina-o uma vez, invoque-o muitas vezes com uma CTM diferente, e os bytes aparecem no arquivo apenas uma vez. A maioria das bibliotecas oculta isso por trás de uma única chamada de posicionamento: O HotPDF registra um bitmap com o AddImage e o posiciona com o ShowImage, pegando um x, y, largura, e altura explícitos em vez de pedir que você construa a matriz manualmente:
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;
Abaixo dessa única linha, a biblioteca escreve o dicionário XObject de imagem, configura a CTM para dimensionar e posicionar o quadrado unitário, e emite Do. O modelo por trás é o que vale a pena conhecer, porque ele explica todo resultado estranho: uma imagem esticada é uma CTM com fatores de escala incompatíveis, um logotipo idêntico em quarenta páginas é um XObject de forma (form XObject) invocado quarenta vezes e uma imagem que é renderizada de cabeça para baixo é uma inversão de sinal (sign flip) na matriz, não um arquivo corrompido
Onde isso nos leva
O modelo gráfico é pequeno uma vez que você vê a sua forma. Um fluxo de conteúdo é bytecode pós-fixado executado contra um estado mutável; as coordenadas começam na parte inferior esquerda e passam pela CTM; os caminhos são construídos silenciosamente e pintados com um operador intencional; as configurações de cor e linha persistem até que você as agrupe (bracket) com q/Q; imagens e gráficos reutilizáveis são XObjects colocados através da transformação de um quadrado unitário. Quase todos os resultados de renderização confusos se resumem a uma dessas cinco regras. Se você quiser ver como esses operadores gráficos se encaixam no modelo de objeto maior, os dicionários de página e a tabela de referência cruzada que apontam para eles, a visão geral técnica da estrutura de arquivos PDF cobre essa camada, e criar um documento PDF simples do zero percorre os bytes de ponta a ponta. O desenho de texto vive em sua própria família de operadores e tem suas próprias armadilhas, cobertas na peça que o acompanha sobre texto em PDF e manipulação de fontes
As chamadas de desenho do Delphi mostradas aqui, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage e ShowImage, fazem parte do Componente HotPDF para Delphi e C++Builder, que emite esses operadores de fluxo de conteúdo para você