Artigo Técnico

Criando PDFs do Zero com o Componente PDFium no Delphi

O PDFium tem a reputação de ser um motor de visualização, o renderizador por trás da aba de PDF do Chrome, então a primeira coisa a esclarecer é que o Componente PDFium também pode construir um documento que nunca existiu antes. O lado da autoria envolve a API page-object do PDFium: você cria um documento vazio, adiciona páginas com dimensões explícitas e solta textos, caminhos vetoriais e imagens em cada página nas coordenadas de sua escolha. Não há nenhuma linguagem de descrição de página a aprender e nenhum driver de impressão no processo. Você chama os métodos, a biblioteca monta os objetos PDF, e SaveAs serializa o resultado

O que você não recebe é um motor de layout. Isso é importante o suficiente para ser dito logo de início, porque molda todos os exemplos a seguir. O Componente PDFium coloca o conteúdo onde você mandar, em coordenadas absolutas, e em nenhum outro lugar. Ele não irá quebrar um parágrafo, não fará o fluxo do texto por uma quebra de página, nem computará uma tabela a partir de linhas e colunas. Esse é o seu trabalho. Se você chegou esperando algo que reformate a prosa do jeito que um processador de texto faz, recalibre as expectativas: esta é uma API de posicionamento preciso de baixo nível, mais próxima de desenhar em um canvas do que de compor um documento. Para gerar faturas, certificados, etiquetas e páginas de relatórios em que você já sabe a qual local pertence cada elemento, essa precisão é exatamente o que você quer

O mínimo para produzir um arquivo

Três chamadas separam um TPdf vazio de um PDF salvo: crie o documento, adicione uma página, e grave-o. Tudo o mais é conteúdo que você adiciona em camadas pelo meio

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Um detalhe engana as pessoas que viram trechos de código mais antigos: você não atribui Pdf.Active := True depois do CreateDocument. A propriedade Active relata se existe um identificador (handle) do documento, e o CreateDocument já criou um, portanto, a propriedade é verdadeira (True) no instante em que aquela chamada retorna. Defini-la de novo é na melhor das hipóteses uma operação nula, e na pior das hipóteses enganoso para o próximo leitor. A propriedade Active prova o seu valor na saída: atribuir False libera o documento subjacente antes de Free, a qual é a ordem correta de encerramento. Trate o CreateDocument e a abertura para carregamento de arquivos como mutuamente exclusivos. A biblioteca se recusa a criar um novo documento em um TPdf que já tem um aberto, portanto, o reuso significa fechar o documento atual primeiro

Coordenadas iniciam no canto inferior esquerdo

O segundo par de argumentos de AddText, e de toda chamada de posicionamento, é um ponto no espaço de usuário do PDF. A origem fica no canto inferior esquerdo da página, o X vai para a direita, e o Y vai para cima. Uma unidade equivale a um ponto, 1/72 de polegada, portanto, uma página A4 tem 595 por 842 unidades e a US Letter tem 612 por 792. Esse Y voltado para cima é a fonte mais comum da confusão de "meu texto está fora da página", pois as coordenadas de tela e bitmap colocam a origem no topo com o Y crescendo para baixo. Em uma página de 842 pontos de altura, um cabeçalho perto do topo fica em volta do Y 780, e não Y 60. Quando uma execução chega em um local inesperado, a altura da página menos o seu Y é quase sempre o número a que você se referia

AddPage toma uma posição de inserção como seu primeiro argumento, expresso em base 1, usando o 0 como um atalho conveniente de "início do documento". Passe 0 ou 1 para a primeira página e a página é inserida na frente; passe o valor que equivale à contagem que você está adicionando para incluir no final. A nova página adicionada também se torna a página atual, que é o alvo das chamadas de desenho subsequentes, assim, não existe um passo separado para "selecionar esta página" depois de adicioná-la. Se você adicionar várias páginas e mais tarde precisar desenhar novamente numa página anterior, defina o PageNumber para mover o cursor; enquanto estiver preenchendo páginas em ordem de criação, você pode deixá-lo quieto

Escrevendo textos, e a regra de fontes que morde silenciosamente

A assinatura do AddText traz tudo de que uma simples execução precisa: a string, um nome de fonte, um tamanho em pontos, a âncora X e Y e, então, opções como cor, um byte alfa para transparência e um ângulo de rotação em graus

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

O byte alfa vai de $00 (invisível) a $FF (opaco), o que faz o selo de rascunho ser uma marca d'água ao invés de um bloco sólido: $30 é aproximadamente dezenove por cento de opacidade, o suficiente para ler o que está embaixo. O ângulo roda no sentido anti-horário em torno de sua âncora, portanto 45 graus proporcionam o selo clássico de ponta a ponta. Nada disso necessita de um recurso separado de marca d'água. Uma marca d'água é simplesmente uma chamada AddText grande, semitransparente e rotacionada, e ao desenhá-la antes ou depois do corpo definimos se ela ficará por trás ou por cima do conteúdo

As fontes merecem uma menção cuidadosa, pois o modo de falha é silencioso. Ao passar o nome de uma fonte, o Componente PDFium pede ao sistema operacional pelos dados TrueType dessa fonte e os incorpora no documento, que é o motivo de um arquivo construído na sua máquina ser renderizado de forma idêntica em outra onde a fonte nunca esteve instalada. O problema ocorre quando o nome não é resolvido: um erro de digitação, ou uma face que simplesmente não está presente na máquina da compilação. Não existe exceção. A biblioteca volta atrás e cria um objeto de texto que traz o nome apenas como um rótulo, sem incorporar nada, e deixa ao leitor substituir por algo que considere parecido. O texto aparece nos seus testes, parece plausível, e muda métricas ou glifos assim que o arquivo é aberto em um local com diferentes fontes instaladas. Use nomes que sabe estarem presentes na máquina geradora, trate a lista de fontes como uma dependência de implementação, e abra um exemplo num visualizador num sistema limpo antes de confiar no resultado

Formas vetoriais: construa um caminho e depois o efetue

Linhas, retângulos e regiões preenchidas passam por um caminho (path). Você abre um com CreatePath, que define o ponto de partida e toda a estilização ao mesmo tempo, modo de preenchimento (fill mode), cores de preenchimento e traço (stroke) com os próprios bytes alfa, largura de traço, extremidades e junções de linha. Em seguida, estenda-o com LineTo, BezierTo e ClosePath e, por último, o AddPath efetua (commits) o caminho finalizado na página. A etapa de cometer a execução é facilmente esquecida e não produz nada se você pular

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

Duas sobrecargas abrangem os casos mais comuns. A forma de quatro coordenadas leva X, Y, largura e altura e entrega um retângulo alinhado ao eixo numa única chamada, o que é útil para desenhar uma regra, uma borda de célula, ou um painel preenchido ao fundo. A forma de duas coordenadas define apenas o ponto inicial, e você mesmo traça o restante do contorno com LineTo e BezierTo. O modo de preenchimento controla como regiões sobrepostas são pintadas: fmWinding (rotação não nula) é adequada a formas sólidas, fmAlternate (par-ímpar) lida com recortes e contornos que se autointersedem, e fmNone deixa o caminho apenas em traço sem preenchimento, que é o que o divisor acima utiliza

Tabelas são caminhos e textos, montados a mão

Pela falta de uma primitiva de tabela, uma tabela é um loop. Você decide os deslocamentos em X para as colunas e a altura da linha, escreve cada célula através do AddText e desenha as regras por meio de caminhos de retângulos. A aritmética é toda sua, mas ela é direta e simples e, ao ser escrita, pode ser estendida para qualquer grade exigida

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Atente-se ao Y se deslocando para baixo conforme a altura de linha em cada passo, também por causa de o para cima ser positivo. É aqui que a ausência de medição de texto é mostrada: nada interrompe um nome longo de item de continuar entrando para a coluna ao lado, porque a biblioteca não sabe quão grande sua string irá se renderizar. Em formatos de saída fixa onde os dados são geridos, é recomendável dimensionar as colunas generosamente e continuar o trabalho. Para conteúdo verdadeiramente variável, você limita os conteúdos de entrada, ou então terá que medir pessoalmente a largura dos glifos antes do posicionamento, que é quando bibliotecas próprias e dedicadas a composições começam a ganhar o seu valor e valerem por si próprias

Imagens e várias páginas

O conteúdo rasterizado é introduzido por meio de facilitadores de imagens. O AddPicture usa um TPicture carregado e o coloca em um ponto, com uma largura e altura opcional para ser dimensionado; o AddImage aceita um caminho de arquivo ou um TBitmap diretamente, e o AddJpegImage permite transmitir os bytes JPEG sem um processo pelo bitmap. Da mesma forma como nas demais opções de criação, as coordenadas em questão para serem colocadas são os locais do canto inferior esquerdo da imagem dentro de seu espaço do usuário, enquanto que as medidas da largura e altura consistem nos tamanhos das páginas medidas nos seus pontos em si, mas não nas proporções e dimensões dos pixels para a origem informada

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Um documento de várias páginas é o padrão de página única em um loop. Cada AddPage adiciona uma página e a torna atual, de modo que o corpo e o rodapé que você desenha a seguir caiam na página que você acabou de adicionar. Você não reatribui PageNumber dentro desse loop, porque adicionar uma página já moveu o cursor para lá; você só precisa do PageNumber quando volta a uma página fora da ordem de criação. Chame SaveAs uma vez no final, depois que a última página for preenchida. Se você precisar de um perfil de arquivamento em vez de um arquivo simples, o mesmo objeto de documento expõe o SaveAsPdfA e as outras variantes de conformidade, portanto, a escolha do padrão de saída é uma chamada de salvamento diferente, não um caminho de compilação diferente

Onde isto se encaixa

A estrutura honesta é que a API de autoria do Componente PDFium é uma camada fina e fiel sobre o modelo page-object do PDFium: criação de documentos reais, fontes incorporadas reais, conteúdo vetorial e rasterizado reais, serializados para um arquivo em conformidade com os padrões. Ele não é e não finge ser um motor de documento com refluxo de texto (reflow). A linha divisória é o layout de texto. Se a sua saída é baseada em modelos, faturas, certificados, rótulos, painéis renderizados para uma grade fixa, o modelo de coordenadas absolutas é direto e rápido e o código permanece legível. Se a sua saída é uma prosa longa que deve quebrar as linhas e paginar sozinha, você estará reconstruindo um motor de layout sobre essas chamadas, e essa é a ferramenta errada para o trabalho. Saber em qual lado dessa linha você está é a maior parte da decisão

Os métodos de criação aqui descritos fazem parte do Componente PDFium para Delphi, que combina esse caminho de autoria com a renderização e com os recursos de extração de texto pelos quais o PDFium é mais conhecido