REPORTLAB Desmistificado
Carlos Leite
Dec. 31, 2020A classe `Canvas`
O ReportLab é uma biblioteca Python para a criação de arquivos PDF.
Dentro do ReportLab existe mais de uma ferramentas para criação de PDF, o `Canvas` é uma delas.
O *namespace* é `reportlab.pdfgen.canvas`
Entre as opções que o ReportLab possui, o objeto `Canvas` é a opção mais rudimentar para a criação de um PDF.
Você pode enxergar o `canvas` como a maneira mais simples de gerar um PDF,
ou a maneira mais trabalhosa de gerenciar a diagramação do conteúdo do PDF.
É simples, porque não necessita de nenhuma preparação prévia, como um template.
É complexo, porque não há nenhuma facilidade para controlar o posicionamento e a diagramação de textos longos ou imagens dentro do `canvas` além é claro do sistema de coordenadas.
Ou seja, mas uma vez, a escolha da ferramenta passa pelo contexto e o objetivo que se quer alcançar.
show me the code...
`class::Canvas` - Instância
O `reportlab.pdfgen.canvas` é um objeto que remete a uma página do seu documento.
Para "criar" um `canvas` é como no código abaixo onde aproveito para descrever os parâmetros passados para a classe.
canvas_obj = canvas.Canvas( f'necto_canvas_demo.pdf', # nome do arquivo PDF a ser gravado pagesize=page_size, # definicao do tamanho o página pageCompression=1, # pdf comprimido ou não - `True` maior tempo de processamento encrypt=None) # se será encriptado ou não
File Name (requerido/obrigatório): *string* indicando o nome do arquivo.
pagesize: *objeto* `reportlab.lib.pagesizes`.
pageCompression: *Boolean* , se PDF será ou não comprimido.
encrypt: *Boolean*, se o PDF será encriptado.
Então, para se produzir um PDF mínimo utilizando o `Canvas` seria o seguinte código.
from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 canvas_obj = canvas.Canvas(f'necto_demo_reportlab_canvas_A4.pdf', pagesize=A4) # imprime ou melhor desenha uma `string` no canvas. # leia sobre este método abaixo canvas_obj.drawString(0,0, '[_-_TEXTO_teste_NECTO_-_]') canvas_obj.save() # cria de fato o arquivo.
Nada mal para 3 linhas de código e 2 `imports`.
Rapidamente ...
- instanciamos um objeto `canvas`
- adicionamos um texto utilizando o `canvas.drawString`
- criamos o PDF de fato com o método `canvas.save`
`class::Canvas` - Métodos iniciais
Vou listar alguns métodos para que seja mais fácil entender os exemplos de código deste documento.
`Canvas.drawString(x, y, text, mode=None, charSpace=0, direction=None, wordSpace=None)`:
"desenha" uma *string* utilizando o estilo de texto do canvas, na posição (x,y).
`Canvas.drawCentredString(x, y, text, mode=None, charSpace=0, direction=None, wordSpace=None)`:
"desenha" uma *string* centralizada na posição (x,y) utilizando o estilo de texto do canvas - o centro horizontal da *string* será posicionado na posição (x,y).
É, o cara que escreveu a função é britânico e centralizado é "centred"
`Canvas.drawImage(image, x, y, width=None, height=None, mask=None, preserveAspectRatio=False, anchor='c', anchorAtXY=False, showBoundary=False)`:
Desenha uma imagem . `image` pode ser uma `string` como caminho para um arquivo ou um objeto do tipo `reportlab.lib.utils.ImageReader`.
`Canvas.grid(xlist, ylist)`:
Desenha um grid e recebe uma lista de coordenadas x e y - é como se as listas x e y, definissem as coordenadas de uma linha diagonal, e o grid faz o resto.
`Canvas.setFont(psfontname, size, leading=None)`:
define a fonte e o tamanho da fonte a ser usada em todo o *canvas*
`Canvas.setFillColor(aColor, alpha=None)`:
Recebe um objeto do tipo `reportlab.lib.colors.Color` e o `alpha` que no final reflete a transparência da cor 1 = 100% transparente.
O objeto dentro de `reportlab.lib.colors` vc tem cores pré-definidas:
'azure', 'beige', 'black', ..., 'blue', 'red', 'yellow' ...
ou ainda pode definir uma cor RGB, CMYK
Ex.: RGB ~ R=29 G=56 B=140 -> `AZUL = colors.Color(29 / 255, 56 / 255, 140 / 255)`
ou ainda utilizar outros 2 métodos ...
**Canvas.setFillColorCMYK(c, m, y, k, alpha=None)**
**Canvas.setFillColorRGB(r, g, b, alpha=None)**
Módulo `reportlab.lib.pagesizes`:
Possui algumas contantes que definem tamanhos de páginas padrão.
ex.:
'A0', ..., 'A9', 'B0', ..., 'B9', 'C0', ..., 'C9', 'ELEVENSEVENTEEN', 'GOV_LEGAL', 'GOV_LETTER', 'HALF_LETTER', 'JUNIOR_LEGAL', 'LEDGER', 'LEGAL', 'LETTER', 'TABLOID',
Os `page_sizes` são tuplas (Altura, Largura)
Reportlab `Canvas` - Coordenadas e Posicionamento
O sistema de coordenadas
O *canvas* funciona com 2 eixos de coordenadas, horizontal e vertical (x,y), onde a posição inicial (0,0) é no canto inferior esquerdo - ou no primeiro quadrante do plano cartesiano.
Nota: A posiçao (0,0) parece pouco intuitivo para quem pensa em um documento textual, mas para gráficos faz todo sentido - "ReportLab" ferramenta para relatórios.
Existe um parâmetro `bottomup = 1` que deveria colocar o canvas no 4º quadrante, ou a posição (0,0) no canto superior esquerdo, mas veja o que o manual diz sobre isso abaixo.
The bottomup argument switches coordinate systems. Some graphics systems (like PDF and PostScript) place (0,0) at the bottom left of the page others (like many graphical user interfaces [GUI's]) place the origin at the top left. The bottomup argument is deprecated and may be dropped in future Need to see if it really works for all tasks, and if not then get rid of it -- reportlab-userguide.pdf
ou seja, Não use o `bottomup = 1` - fique no (0,0) canto inferior esquerdo.
Outra maneira de "mover" a referência seria utilizar o método `Canvas.translate(x,y)`
Na prática ...
# script grid de coordenadas from reportlab.pdfgen import canvas from reportlab.lib.units import inch from reportlab.lib.units import cm from reportlab.lib.colors import Color, black, blue, red, white, yellow # nome do PDF de saida. output_pdf = 'necto_demo_reportlab_canvas_custom_grid.pdf' # instancia canvas com tamanho customimzado de 20cm por 10cm canvas_obj = canvas.Canvas(output_pdf, pagesize=(20 * cm, 10 * cm)) canvas_obj.setLineWidth(1) canvas_obj.setStrokeGray(0.6) canvas_obj.grid(range(0, 120, 5), range(0, 120, 10)) canvas_obj.setStrokeColor(blue) canvas_obj.grid([x * inch for x in range(0, 100, 1)], [y * inch for y in range(0, 100, 1)]) canvas_obj.setStrokeColor(red) canvas_obj.grid([x * cm for x in range(0, 30, 1)], [y * cm for y in range(0, 30, 1)]) canvas_obj.drawString(0, 0, 'NECTO DEMO ReportLab Canvas Grid - Units, Inches and Centimeters') canvas_obj.save()
resultado seria um documento PDF como abaixo, sendo que:
- A linhas em cinza claro formam um grid o qual a distância é de **10** unidades de medida do canvas.
- As linhas em azul, estão em polegadas e o grid Azul é de **1** polegada.
- As linhas em vermelho são do grid com espaçamento de **1** cm.

Repare o texto na posição (0,0) - `canvas_obj.drawString(0, 0, 'NECTO DEMO Re...` está no canto inferior esquerdo do documento gerado.
não tente medir o grid com uma régua, a imagem foi reduzida para este documento
Alternativa ao `Canvas( ..., bottomup=True)` ~ `Canvas.translate(x,y)`
Adicionando a linha em destaque abaixo ao script que produz o Grid de coodenadas, provocamos o deslocamento da referência (0,0) para o centro da página.
`Canvas.translate(dx, dy)`:
Move a origem do ponto corrente (0,0) para o ponto (dx,dy).
# ... canvas_obj.setLineWidth(1) # >>>> linhas em destaque ;) ponto_medio_eixo_x = canvas_obj._pagesize[0]/2 # oops `._pagesize` ? ponto_medio_eixo_y = canvas_obj._pagesize[1]/2 canvas_obj.translate(ponto_medio_eixo_x, ponto_medio_eixo_y) canvas_obj.setStrokeGray(0.6) # ...
- acessei o atributo "protegido" `_pagesize` para ter acesso a tupla que define o tamaho da página (repita por sua própria conta e risco).
- cada elemento da tupla foi dividido por 2, ou seja, o ponto médio de cada eixo (x e y)
- tendo o ponto do meio de cada eixo, passei ao método translate .

Importante: O fato de um objeto estar fora da área útil do canvas não implica em erro.
Repare que apesar de o texto e nem mesmo os pontos do grid "caberem" dentro do canvas, não houve nenhum erro, ou alteração automática no tamanho do canvas!
O controle da posição, e se a posição está dentro da área visivel do *Canvas* é de sua responsabilidade. Por tanto, dependendo do conteúdo a ser colocado no `Canvas`, a gestão do layout ou diagramação, pode se tornar bastante complexa. Principalmente com textos de vários parágrafos, onde ficará difícil determinar a posição final que este texto ocupa.
O método translate, é particularmente útil em um código modularizado, onde o `Canvas` teria "sub-areas", e seria necessário controlar o posicionamento dentro destas áreas sem considerar o todo - Não vou me aprofundar nisso, não creio que seja um tópico necessário para o entendimento mínimo do `Canvas`, mas fica a dica.
Canvas - Posicionando Textos
from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 page_size = A4 canvas_obj = canvas.Canvas( f'necto_cancas_demo_500x500.pdf', pagesize=(500,500), pageCompression=1, encrypt=None) canvas_obj.drawString(0,0, '[_-_TEXTO_teste_NECTO_inferior_esquerdo_-_]') canvas_obj.drawString(500,0, '[_-_TEXTO_teste_NECTO_inferior_direito_-_]') canvas_obj.drawString(0,500, '[_-_TEXTO_teste_NECTO_-_superior_esquerdo]') canvas_obj.drawString(500,500, '[_-_TEXTO_teste_NECTO_-_superior_direito]') canvas_obj.drawString(250,250, '[_-_TEXTO_teste_NECTO_-_CENTRO]') canvas_obj.save()
e o resultado é

nada como esperado, ou pelo menos em uma primeira vista.
O código acima falhou retumbantemente - segundo minhas expectativas iniciais.
O código funciona como deveria, mas o que deveria estar no centro não está centralizado (250,250) , os textos nas posições superiores não aparecem no documento e nem o texto no canto inferior direito.
Os textos também tem como referência seu ponto inferior a esquerdo, então para todas as posições temos que descontar a altura e ou a largura do texto.
.. code-block:: python from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 page_size = A4 canvas_obj = canvas.Canvas( f'necto_cancas_demo_500x500.pdf', pagesize=(500,500), pageCompression=1, encrypt=None) canvas_obj.drawString(0,0, '[_-_TEXTO_teste_NECTO_inferior_esquerdo_-_]') canvas_obj.drawString(300,0, '[_-_TEXTO_teste_NECTO_inferior_direito_-_]') canvas_obj.drawString(0,480, '[_-_TEXTO_teste_NECTO_-_superior_esquerdo]') canvas_obj.drawString(500,450, '[_-_TEXTO_teste_NECTO_-_superior_direito]') canvas_obj.drawString(150,250, '[_-_TEXTO_teste_NECTO_-_CENTRO]') canvas_obj.save()

uma melhora razoável, mas ainda pouco eficaz e nada eficiente. Se alterarmos o texto, vai tudo pro espaço.
Dica: existe um método para centralizar textos no canvas, `canvas.drawCentredString(x_sini, Y_ini, string)`.
Tamanho da String no canvas - `pdfmetrics.stringWidth`
Namespace: `from reportlab.pdfbase.pdfmetrics`
Então podemos usar o método `stringWidth` para calcular o tamanho do texto previamente, e retorna um inteiro.
# o método `stringWidth` from reportlab.pdfbase import pdfmetrics pdfmetrics.stringWidth(text, fontName, fontSize, encoding='utf8')
Para usar o método `stringWidth`, precisamos conhecer o texto, o nome da fonte, o tamanho da fonte e o *encoding*.
Infelizmente a API do `reportlab.pdfgen.canvas` não é muito boa quando se quer recuperar valores setados previamente.
Por exemplo, `fontname`,
In [84]: c.setFont("Helvetica", 14) In [86]: c._fontname # atributo com protegido ("_") Out[86]: 'Helvetica' In [87]: c._fontsize Out[87]: 14
Como os atributos setados inicial com um 'underscore' ("_") em teoria não deveriam ser acessados pelo usuário da API.
... no fundo isso diz: "use por sua conta e risco - nós os autores não garantimos nada!"
Então é melhor definirmos explicitamente e guardar as definições.
from reportlab.pdfgen import canvas from reportlab.lib import units from reportlab.pdfbase import pdfmetrics # defninção da área do canvas canvas_width = 20 * units.cm canvas_height = 20 * units.cm page_size = (canvas_width, canvas_height) # area do canvas font_name = 'Helvetica' # nome da fonte font_size = 12 # tamanho da conte canvas_obj = canvas.Canvas( f'necto_canvas_demo_20x20_positioning.pdf', pagesize=page_size, pageCompression=1, encrypt=None) canvas_obj.setFont(font_name, font_size) # canvas def da fonte e tamanho text = '[_-_NECTO_-_]' text_width = pdfmetrics.stringWidth(text,font_name, font_size) canvas_obj.drawString(0, canvas_height - font_size, f'{text}') canvas_obj.drawString(canvas_width - text_width , canvas_height - font_size, f'{text}') canvas_obj.drawString((canvas_width - text_width) / 2, canvas_height/2, f'{text}') canvas_obj.drawString(0, 0, f'{text}') canvas_obj.drawString(canvas_width-text_width, 0, f'{text}') canvas_obj.save()
Baseado no código acima, posso organizar meu código em funções,
podendo reaproveitar definições como o "título" po exemplo,
ou definir uma função para cada página.
Usando a mesta instância do objeto *canvas*, eu posso criar várias páginas em um mesmo PDF.
importante!
o método `save` do canvas deve ser chamado uma única vez,
do contrário ocorre uma exceção do tipo `RunTimeError`.
RuntimeError: class PDFDocument instances can only be saved once
Considerações sobre o `reportlab.pdfgen.canvas`
A API do `reportlab.pdfgen.canvas` é a API de mais baixo nível para se criar
um arquivo PDF com o ReportLab.
Em outras palavras, é maneira mais "tosca" dentre as opções que a biblioteca
Reportlab disponibiliza para criar um PDF.
Mas como quase qualquer ferramenta, em software ou não
maior controle geralmente implica em maior complexidade - e você assume os riscos!
Praticamente todo o controle do "o que", "o onde" e "o como" um objeto será inserido no PDF, deve ser manualmente verificado e controlado pelo código.
Por isso eu diria que o canvas `reportlab.pdfgen.canvas` não é a solução de criação de PDFs para grandes conteúdos de textos.
Eu diria que seu uso é indicado quando houver a necessidade de uma página rebuscada graficamente mas ao mesmo tempo, ainda de simples controlar o espaço que seu conteúdo ocupa nesta página.
O `reportlab.pdfgen.canvas` pode ser uma saída rápida e prática. Ex.: Certificado de conclusão de curso, ou um portfólio simples. Qualquer coisa com poucas páginas e que não há um padrão de formato muito rígido entre as páginas.
.. tip:: muitas páginas, muito conteúdo, e formato padrão ? veja o módulo `reportlab.platypus` em Platypus faq
Mas isso não quer dizer que o ReportLab seja uma ferramenta menor ...
É poderoso e por isso acredito que ainda vale a pena investir no seu uso.
Para aqueles que trabalham em projeto de software sob-demanda ou software customizado,
ter o controle total do documento em detrimento da facilidade de controle pode auxiliar
em um software customizado que se encaixa melhor nas necessidades do seu cliente,
dando ao cliente exatamente aquilo que ele precisa