Skip to main content

REPORTLAB Desmistificado

Carlos Leite

Dec. 31, 2020

A 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 ...

  1. instanciamos um objeto `canvas`
  2. adicionamos um texto utilizando o `canvas.drawString`
  3. 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.

necto_demo_reportlab_canvas_custom_grid_500PX.png

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)
    # ...
  1. 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).
  2. cada elemento da tupla foi dividido por 2, ou seja, o ponto médio de cada eixo (x e y)
  3. tendo o ponto do meio de cada eixo, passei ao método translate .

necto_demo_reportlab_canvas_custom_grid_displaced.png

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 é

reportlab_500x500_positioning.png

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()

reportlab_500x500_positioning_desc_texto.png

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


Please sign in

Signin to manage your account.

Do not have an account? Signup

OR