Friday, October 1, 2021

Stack Abuse: Create a Stunning PDF Flyer in Python with borb

The Portable Document Format (PDF) is not a WYSIWYG (What You See is What You Get) format. It was developed to be platform-agnostic, independent of the underlying operating system and rendering engines.

To achieve this, PDF was constructed to be interacted with via something more like a programming language, and relies on a series of instructions and operations to achieve a result. In fact, PDF is based on a scripting language - PostScript, which was the first device-independent Page Description Language.

In this guide, we'll be using borb - a Python library dedicated to reading, manipulating and generating PDF documents. It offers both a low-level model (allowing you access to the exact coordinates and layout if you choose to use those) and a high-level model (where you can delegate the precise calculations of margins, positions, etc to a layout manager).

In this guide, we'll take a look at how to generate a flyer containing custom graphics (represented by PDF operators).

Installing borb

borb can be downloaded from source on GitHub, or installed via pip:

$ pip install borb

What We’ll Be Making

It's oftentimes easier to make a sketch, and work towards it, rather than building blind, so feel free to sketch out a flyer on a piece of paper you have lying around, and letting that creativity flow unto the canvas.

We'll be making a flyer like this, to promote a supposed product belonging to a supposed company:

Example Flyer Image

Creating a PDF Document with borb

Building a PDF document in borb typically follows the same couple of steps:

  • Creating an empty Document
  • Creating an empty Page and appending it to the Document
  • Setting a PageLayout on the Page
  • Adding content to the PageLayout
  • Persisting the Document

Let's see what that looks like in code:

from borb.pdf.document import Document
from borb.pdf.page.page import Page
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from borb.pdf.canvas.layout.page_layout.page_layout import PageLayout
from borb.pdf.pdf import PDF

def main():
  # Create empty Document
  pdf = Document()

  # Create empty Page
  page = Page()

  # Add Page to Document
  pdf.append_page(page)

  # Create PageLayout
  layout: PageLayout = SingleColumnLayout(page)

  # Future content-rendering-code to be inserted here
  
  # Attempt to store PDF
  with open("output.pdf", "wb") as pdf_file_handle:
      PDF.dumps(pdf_file_handle, pdf)
  
if __name__ == '__main__':
  main()

Creating a PDF Flyer with borb

Now that we have an empty canvas to work from, let's add the basic content. We'll start by adding the title, such as "Your Company":

# New imports
from borb.pdf.canvas.layout.text.paragraph import Paragraph  
from borb.pdf.canvas.color.color import HexColor  
from decimal import Decimal  
  
# Contact information
layout.add(
  Paragraph("Your Company", 
            font_color=HexColor("#6d64e8"), 
            font_size=Decimal(20)
    )
)

The next step is adding the QR-code and the contact information. In order to easily present this content side by side, we're going to be using a Table.

We also need the coordinates of the QR-code (we'll be adding something special to it later on). So let's start by declaring that first:

# New imports
from borb.pdf.canvas.layout.image.barcode import Barcode, BarcodeType  
from borb.pdf.canvas.layout.layout_element import LayoutElement
  
# Code to generate a QR code LayoutElement
qr_code: LayoutElement = Barcode(
    data="https://www.borbpdf.com",
    width=Decimal(64),
    height=Decimal(64),
    type=BarcodeType.QR,
)

Now we can build and add our Table:

 # New imports
from borb.pdf.canvas.layout.table.flexible_column_width_table import FlexibleColumnWidthTable
  
layout.add(
    FlexibleColumnWidthTable(number_of_columns=2, number_of_rows=1)
    .add(qr_code)
    .add(
        Paragraph(
            """
            500 South Buena Vista Street
            Burbank CA
            91521-0991 USA
            """,
            padding_top=Decimal(12),
            respect_newlines_in_text=True,
            font_color=HexColor("#666666"),
            font_size=Decimal(10),
        )
    )
    .no_borders()
)

Let's run that code and see what the generated PDF looks like. I find that the best way of tweaking the little UI/UX details.

creating a pdf document with borb

Looking good! The QR-code is situated right under the company's name, contains the right contact information and actually encodes the contact data we've provided.

Next we're going to add a remote go-to annotation. That's just PDF-talk for "a clickable link that takes you outside the PDF".

We're going to make sure the entire QR-code is actually a link that takes the reader to our website. That way, if they have the printed version of this PDF they can simply scan the QR code. If they have the digital version, they can click the QR code.

This is a simple addition, but makes navigation on the user-end a more pleasant experience:

page.append_remote_go_to_annotation(
  qr_code.get_bounding_box(), uri="https://www.borbpdf.com"
)

Adding Product Information

We can now add the next title and subtitle(s), pertaining to a product we're creating a flyer for:

# Title
layout.add(
    Paragraph(
        "Productbrochure", font_color=HexColor("#283592"), font_size=Decimal(34)
    )
)

# Subtitle
layout.add(
    Paragraph(
        "September 4th, 2021",
        font_color=HexColor("#e01b84"),
        font_size=Decimal(11),
    )
)

And similarly, we'll add the product overview title, and some dummy text:

# product overview
layout.add(
    Paragraph(
        "Product Overview", font_color=HexColor("000000"), font_size=Decimal(21)
    )
)

layout.add(
   Paragraph(
        """
        Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. 
        Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. 
        A small river named Duden flows by their place and supplies it with the necessary regelialia.
        """
    )
)

layout.add(
    Paragraph(
        """
        It is a paradisematic country, in which roasted parts of sentences fly into your mouth. 
        Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life. 
        One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar.
        """,
        margin_bottom=Decimal(12)
    )
)

Note: Pay attention to the last Paragraph where we explictly added a bottom margin. That's just a small visual tweak to ensure there's a bit more room between that Paragraph and the next piece of content, which will be an image.

When we run this code, we should get something like this:

adding information

Finally, we can add the product information. We could have an Image alongside a list of some of the features of the product. So again, we can use a Table to achieve the side-by-side look.

Oftentimes, there's a title above the list of features, so we're going to have a Table with 2 columns (image and features) and 2 rows (one for the title, and one for the features).

Since the table is being used not as a table, but rather just to achieve the side-by-side look, we won't be adding a border to the table:

# New imports
from borb.pdf.canvas.layout.image.image import Image
from borb.pdf.canvas.layout.table.table import TableCell  
from borb.pdf.canvas.layout.table.fixed_column_width_table import FixedColumnWidthTable
from borb.pdf.canvas.layout.list.unordered_list import UnorderedList
  
# Table with image and key features
layout.add(
    FixedColumnWidthTable(
        number_of_rows=2,
        number_of_columns=2,
        column_widths=[Decimal(0.3), Decimal(0.7)],
    )
    .add(
        TableCell(
            Image(
                  "https://www.att.com/catalog/en/skus/images/apple-iphone%2012-purple-450x350.png",
                width=Decimal(128),
                height=Decimal(128),
            ),
            row_span=2,
        )
    )
    .add(
        Paragraph(
            "Key Features",
            font_color=HexColor("e01b84"),
            font="Helvetica-Bold",
            padding_bottom=Decimal(10),
        )
    )
    .add(
        UnorderedList()
        .add(Paragraph("Nam aliquet ex eget felis lobortis aliquet sit amet ut risus."))
        .add(Paragraph("Maecenas sit amet odio ut erat tincidunt consectetur accumsan ut nunc."))
        .add(Paragraph("Phasellus eget magna et justo malesuada fringilla."))
        .add(Paragraph("Maecenas vitae dui ac nisi aliquam malesuada in consequat sapien."))
    )
    .no_borders()
)

Again, we've added a padding_bottom in some cells of the Table just to provide some extra space. The resulting PDF is almost finished:

The final steps remaining are adding the artwork in the top-right corner, and in the footer.

Using the Shape object in borb

borb can render any Shape to the Page. Shape represents an arbitrary sequence of points (represented as typing.Tuple[Decimal, Decimal]) all of which form a continuous line. This means that you can get quite creative with the shapes you want to create.

We'll start by defining a method that renders the triangles and squares in the upper right corner of the Page:

# New imports
from borb.pdf.canvas.geometry.rectangle import Rectangle
from borb.pdf.canvas.layout.image.shape import Shape
from borb.pdf.page.page_size import PageSize
import typing
import random
  
  
def add_gray_artwork_upper_right_corner(page: Page) -> None:
  """
  This method will add a gray artwork of squares and triangles in the upper right corner
  of the given Page
  """
    grays: typing.List[HexColor] = [
        HexColor("A9A9A9"),
        HexColor("D3D3D3"),
        HexColor("DCDCDC"),
        HexColor("E0E0E0"),
        HexColor("E8E8E8"),
        HexColor("F0F0F0"),
    ]
    ps: typing.Tuple[Decimal, Decimal] = PageSize.A4_PORTRAIT.value
    N: int = 4
    M: Decimal = Decimal(32)
    
    # Draw triangles
    for i in range(0, N):
        x: Decimal = ps[0] - N * M + i * M
        y: Decimal = ps[1] - (i + 1) * M
        rg: HexColor = random.choice(grays)
        Shape(
            points=[(x + M, y), (x + M, y + M), (x, y + M)],
            stroke_color=rg,
            fill_color=rg,
        ).layout(page, Rectangle(x, y, M, M))
        
    # Draw squares
    for i in range(0, N - 1):
        for j in range(0, N - 1):
            if j > i:
                continue
            x: Decimal = ps[0] - (N - 1) * M + i * M
            y: Decimal = ps[1] - (j + 1) * M
            rg: HexColor = random.choice(grays)
            Shape(
                points=[(x, y), (x + M, y), (x + M, y + M), (x, y + M)],
                stroke_color=rg,
                fill_color=rg,
            ).layout(page, Rectangle(x, y, M, M))

We can now call this method in the main method, and give our PDF some extra pazzaz:

Similarly, we could add some graphics to the bottom of the page:

  • A line to separate the footer from the main content of the page
  • A small geometric element to balance the geometric graphic atop the page

Let's write another method to do all of that:

from borb.pdf.canvas.line_art.line_art_factory import LineArtFactory

def add_colored_artwork_bottom_right_corner(page: Page) -> None:
  """
  This method will add a blue/purple artwork of lines 
  and squares to the bottom right corner
  of the given Page
  """
    ps: typing.Tuple[Decimal, Decimal] = PageSize.A4_PORTRAIT.value
    
    # Square
    Shape(
      points=[
          (ps[0] - 32, 40),
          (ps[0], 40),
          (ps[0], 40 + 32),
          (ps[0] - 32, 40 + 32),
      ],
      stroke_color=HexColor("d53067"),
      fill_color=HexColor("d53067"),
    ).layout(page, Rectangle(ps[0] - 32, 40, 32, 32))
    
    # Square
    Shape(
      points=[
          (ps[0] - 64, 40),
          (ps[0] - 32, 40),
          (ps[0] - 32, 40 + 32),
          (ps[0] - 64, 40 + 32),
      ],
      stroke_color=HexColor("eb3f79"),
      fill_color=HexColor("eb3f79"),
    ).layout(page, Rectangle(ps[0] - 64, 40, 32, 32))
    
    # Triangle
    Shape(
      points=[
          (ps[0] - 96, 40),
          (ps[0] - 64, 40),
          (ps[0] - 64, 40 + 32),
      ],
      stroke_color=HexColor("e01b84"),
      fill_color=HexColor("e01b84"),
    ).layout(page, Rectangle(ps[0] - 96, 40, 32, 32))
        
    # Line
    r: Rectangle = Rectangle(Decimal(0), Decimal(32), ps[0], Decimal(8))
    Shape(
      points=LineArtFactory.rectangle(r),
      stroke_color=HexColor("283592"),
      fill_color=HexColor("283592"),
    ).layout(page, r)

Again, we can call this method from the main method. The resulting page should look like this:

Conclusion

In this guide we've taken a look at some of the basic building blocks of PDF documents using borb. We've configured padding and margin, as well as font-size and font-color. We've also generated graphics using the Shape object, and a working clickable QR-code.

With these building blocks, we've created a flyer for a supposed product of an imaginary company, automating the process of creating interactive PDF documents.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...