Technical Article

Criar PDFs de Raiz com o PDFium VCL em Delphi

O PDFium tem reputação como motor de visualização, sendo o renderizador por trás do separador de PDF do Chrome. Por isso, a primeira coisa a esclarecer é que o PDFium VCL também pode construir um documento que nunca existiu anteriormente. O lado de criação envolve a API de objetos de página do PDFium: cria um documento vazio, adiciona páginas com dimensões explícitas e insere texto, caminhos vetoriais e imagens em cada página em coordenadas que escolhe. Não há uma linguagem de descrição de página a aprender nem controladores de impressão envolvidos. Efetua chamadas de métodos, a biblioteca monta os objetos PDF e o SaveAs grava o resultado final.

O que não obtém é um motor de layout. Isto é importante referir desde já, porque influencia todos os exemplos seguintes. O PDFium VCL coloca o conteúdo exatamente onde lhe indica, em coordenadas absolutas, e em mais lado nenhum. Não irá quebrar parágrafos automaticamente, fluir texto entre páginas ou calcular uma tabela com base em linhas e colunas. Essas tarefas são da sua responsabilidade. Se esperava algo que formate prosa da mesma forma que um processador de texto, calibre as expectativas agora: trata-se de uma API de posicionamento precisa e de baixo nível, mais próxima do desenho num canvas do que da paginação automática de um documento. Para faturas, certificados, etiquetas e relatórios gerados onde já sabe exatamente onde cada elemento pertence, essa precisão é exatamente o que procura.

O mínimo necessário para gerar um ficheiro

Apenas três chamadas separam um TPdf vazio de um PDF gravado: criar o documento, adicionar uma página e gravar no disco. Tudo o resto é o conteúdo que insere entre estes passos.

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 confunde as pessoas que viram exemplos mais antigos: não deve atribuir Pdf.Active := True após CreateDocument. A propriedade Active indica se existe um descritor (handle) de documento, e o CreateDocument já criou um, pelo que a propriedade fica ativa no momento em que a chamada retorna. Definir a propriedade novamente não tem qualquer efeito prático e pode induzir em erro quem ler o código posteriormente. A propriedade Active é útil no encerramento: atribuir False liberta o documento subjacente antes do Free, o que representa a ordem de destruição correta. Trate o CreateDocument e o carregamento de ficheiros como operações mutuamente exclusivas. A biblioteca recusa-se a criar um novo documento num TPdf que já tenha outro aberto, pelo que a reutilização exige o fecho prévio do documento atual.

As coordenadas começam no canto inferior esquerdo

O segundo par de argumentos do AddText, e de qualquer chamada de posicionamento, é um ponto no espaço do utilizador do PDF. A origem situa-se no canto inferior esquerdo da página, o eixo X corre para a direita e o eixo Y corre para cima. Uma unidade equivale a um ponto (1/72 de polegada), pelo que uma página A4 tem 595 por 842 unidades e o formato Letter americano tem 612 por 792. Esse eixo Y a crescer para cima é a fonte mais comum de confusão do tipo \"o meu texto está fora da página\", porque as coordenadas do ecrã e de bitmaps colocam a origem no topo com o Y a crescer para baixo. Numa página com 842 pontos de altura, um cabeçalho perto do topo situa-se por volta de Y 780, e não Y 60. Quando um elemento aparece num local inesperado, a altura da página menos o seu Y é quase sempre a coordenada que pretendia definir.

O AddPage recebe uma posição de inserção como o seu primeiro argumento, expressa com base em 1, com o 0 a servir como um atalho conveniente para \"início do documento\". Passe 0 ou 1 para la primeira página e ela será inserida no início; passe o valor correspondente à contagem atual para anexar a página no final. A página recém-adicionada também se torna a página ativa, que será o alvo das chamadas de desenho seguintes, pelo que não existe um passo separado de seleção após a adição. Se adicionar várias páginas e mais tarde necessitar de desenhar numa página anterior, defina o PageNumber para mover o cursor; se estiver a preencher as páginas por ordem à medida que as cria, pode ignorar este passo.

Escrever texto e a regra de fontes que falha silenciosamente

A assinatura do AddText contém tudo o que uma única sequência de texto necessita: a string, o nome da fonte, o tamanho em pontos, as coordenadas X e Y, além de cor opcional, um byte de opacidade (alpha) para transparência e o â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 alpha varia entre $00 (invisível) e $FF (opaco), o que torna a marca de água um elemento semitransparente em vez de um bloco sólido: $30 equivale a cerca de dezanove por cento de opacidade, o suficiente para ler o texto por trás. O ângulo roda a sequência no sentido contrário ao dos ponteiros do relógio em torno do seu ponto de ancoragem, pelo que 45 graus aplica a inclinação clássica de canto a canto. Nada disto exige uma funcionalidade dedicada de marca de água. Uma marca de água é apenas uma chamada AddText com tamanho grande, semitransparente e rotacionada, e desenhá-la antes ou depois do corpo define se ela fica atrás ou por cima do conteúdo principal.

As fontes merecem uma atenção especial, pois o modo de falha é silencioso. Quando passa o nome de uma fonte, o PDFium VCL solicita ao sistema operativo os dados TrueType da mesma e incorpora-os no documento, razão pela qual um ficheiro gerado na sua máquina é renderizado exatamente igual numa que nunca tenha tido a fonte instalada. O problema ocorre quando o nome não é resolvido: devido a um erro ortográfico ou a uma fonte que não esteja presente na máquina de compilação. Não é gerada qualquer exceção. A biblioteca reverte para a criação de um objeto de texto que carrega o nome apenas como etiqueta, sem incorporar nada, e deixa o visualizador substituir por algo que considere semelhante. O texto surge nos seus testes, parece correto, mas as métricas ou os glifos alteram-se assim que o ficheiro é aberto num sistema com fontes diferentes instaladas. Utilize nomes que sabe estarem presentes na máquina geradora, trate a lista de fontes como uma dependência de implementação e abra uma amostra num visualizador num sistema limpo antes de considerar o ficheiro finalizado.

Formas vetoriais: construir um caminho e depois aplicá-lo

Linhas, retângulos e regiões preenchidas são desenhados através de caminhos. Abre-se um caminho com o CreatePath, que define o ponto de partida e toda a formatação de uma só vez (modo de preenchimento, cores de preenchimento e contorno com os seus bytes alpha, espessura da linha, remates e junções). Em seguida, prolonga-se o caminho com LineTo, BezierTo e ClosePath e, finalmente, o método AddPath aplica o caminho terminado na página. Este passo de aplicação é fácil de esquecer e não desenha nada se for ignorado.

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 cobrem os casos comuns. A forma de quatro coordenadas recebe X, Y, largura e altura e fornece um retângulo alinhado com os eixos numa única chamada, o que é ideal para desenhar uma linha de divisão, a borda de uma célula ou um painel de fundo preenchido. A forma de duas coordenadas define apenas o ponto de partida e o utilizador traça o resto do contorno manualmente com LineTo e BezierTo. O modo de preenchimento controla a forma como as regiões sobrepostas são pintadas: o fmWinding (regra diferente de zero) adequa-se à maioria das formas sólidas, o fmAlternate (regra par-ímpar) gere recortes e contornos que se cruzam, e o fmNone deixa apenas a linha de contorno sem preenchimento, que é o que a linha divisória acima utiliza.

As tabelas são construídas manualmente com caminhos e texto

Como não existe uma primitiva de tabela, uma tabela é desenhada através de um ciclo. O utilizador decide os desvios de posição X das colunas e a altura da linha, escreve cada célula com AddText e desenha as linhas de contorno com caminhos retangulares. O cálculo aritmético é feito por si, mas é simples e, uma vez escrito, aplica-se a qualquer grelha que necessite.

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;

Note que o Y desce pela altura da linha em cada passagem, mais uma vez porque o sentido positivo é para cima. É também aqui que a ausência de medição de texto se faz notar: nada impede que o nome longo de um item ultrapasse o limite e se sobreponha à coluna seguinte, porque a biblioteca não sabe a largura com que a string foi renderizada. Para relatórios com formatos fixos onde controla os dados, define as colunas com tamanhos generosos. Para conteúdo verdadeiramente variável, terá de limitar os dados de entrada ou medir a largura dos glifos manualmente antes de os posicionar, que é o ponto em que uma biblioteca de composição dedicada começa a justificar-se.

Imagens e páginas múltiplas

O conteúdo rasterizado é inserido através de funções auxiliares de imagem. O AddPicture recebe um TPicture carregado e posiciona-o num ponto, com largura e altura opcionais para o redimensionar; o AddImage aceita diretamente o caminho de um ficheiro ou um TBitmap, e o AddJpegImage transfere os bytes JPEG diretamente sem passar por um bitmap intermediário. Tal como em tudo o resto, as coordenadas de posicionamento representam o canto inferior esquerdo da imagem no espaço do utilizador, e a largura e altura indicam o tamanho na página em pontos, não as dimensões em píxeis da imagem original.

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 multipágina consiste no padrão de página única executado dentro de um ciclo. Cada AddPage anexa uma página e torna-a ativa, pelo que o corpo e o rodapé que desenhar em seguida serão colocados na página que acabou de adicionar. Não precisa de reatribuir o PageNumber dentro deste ciclo, porque a adição da página já moveu o cursor para lá; só necessita do PageNumber quando pretender regressar a uma página fora da ordem de criação. Chame o SaveAs uma única vez no final, após preencher a última página. Se necessitar de um perfil de arquivo em vez de um ficheiro simples, o mesmo objeto de documento disponibiliza o SaveAsPdfA e outras variantes de conformidade, pelo que a escolha do padrão de saída é apenas uma chamada de gravação diferente e não um caminho de compilação distinto.

Onde esta solução se enquadra

A perspetiva realista é que a API de criação do PDFium VCL é uma camada direta e simples sobre o modelo de objetos de página do PDFium: permite a criação de documentos reais, a incorporação de fontes verdadeiras e de conteúdo vetorial e rasterizado, serializado num ficheiro em conformidade com as normas. Não é, nem pretende ser, um motor de paginação automática de documentos. A linha divisória é o layout do texto. Se o seu resultado for baseado em modelos fixos (faturas, certificados, etiquetas ou painéis dispostos numa grelha definida), o modelo de coordenadas absolutas é direto, rápido e o código permanece legível. Se o seu resultado for prosa de formato longo que necessite de quebra de linha e paginação automática, terá de reconstruir um motor de layout sobre estas chamadas, o que não é a ferramenta indicada para a tarefa. Saber em que lado dessa linha se encontra representa a maior parte da decisão.

Os métodos de criação aqui descritos fazem parte do Componente PDFium VCL para Delphi, que combina este caminho de criação com as funcionalidades de renderização e extração de texto pelas quais o PDFium é mais conhecido.