Tutorial: Two Quick and Easy Ways to Convert Images to PDF in Python
6 min read

Tutorial: Two Quick and Easy Ways to Convert Images to PDF in Python

I recently needed to convert image data to PDFs, which led me down a rabbit hole for like 2 days. Here is what I learnt and how I did it.
Tutorial: Two Quick and Easy Ways to Convert Images to PDF in Python

A Primer on Images

First, let's understand how images are represented in data.

Raster vs Vector Images

Raster images are composed of pixels. A pixel is the smallest square in a raster image containing image information. This can include colour and opacity. The more pixels an image has, the better the quality. Images with a low pixel count have a lower resolution and can appear to have jagged edges. They cannot be scaled up without a loss of resolution.
Raster images are compatible with many devices/software.

Examples of raster image file formats are JPEG, PNG, TIFF, GIF and BMP.

Vector images on the other hand are composed of curves and paths created by mathematical formulas. Due to this, they are infinitely scalable without losing any resolution. These images are typically not supported by many devices and are usually tied to specific software such as Adobe Illustrator.

Because of them being made up of mathematical formulas, their file sizes are usually smaller than their raster counterparts.

Examples of vector image file formats are SVG, ai (Adobe Illustrator) and eps (Encapsulated PostScript).

We'll focus on raster images in this post.

Pixels

A pixel is the smallest unit of a raster image, typically a small square. Image dimensions are typically represented in the format width px x height px based on the number of pixels in the x and y axes. So if an image is sized 100px x 200px, it has 100 pixels on the x-axis and 200 pixels on the y-axis and a total of 20,000 individual pixels.

Bands

An image can consist of one or more bands of data for each of its pixels. For example, a PNG image might have ‘R’, ‘G’, ‘B’, and ‘A’ bands for the red, green, blue, and alpha transparency values.

Pixel/Bit Depth

This refers to the amount of information that can be stored in every pixel. For instance, 1-bit depth means that the possible range of values that can be store in the pixel is 2^1 = 2. This is commonly used in black and white images with no variation in opacity. An 8-bit depth means 2^8 = 256 possible variations in a single pixel. A JPEG image with the RGB band has 3x8 bits, one for each color band.

This article goes much deeper into these fundamental image concepts. But the primer here is enough to follow along with the rest of the post.

First, lets set up a virtual environment for our project

python3 -m venv image_tutorial
source venv/bin/activate

We'll use this image by Charles Deluvio on Unsplash.

Download it and put it in the same directory as your virtual environment.

Convert an Image to PDF the Easy Way

If all you want is to convert an image to PDF without making extensive image changes, img2pdf is a great and efficient package that does this for you with very little code.

Let's first install it to our virtualenv then get back into our Python shell.

pip install img2pdf

First, we'll specify the A4 page (8.3 x 11.7 inches) size using some included sizing utilities.

import img2pdf

a4_page_size = [img2pdf.in_to_pt(8.3), img2pdf.in_to_pt(11.7)]
a4_page_size
# output: [597.6, 842.4]
layout_function = img2pdf.get_layout_fun(a4_page_size)

Finally, create the PDF file in memory and write it out into a file.

pdf = img2pdf.convert('<<<insert image file name/path>>>', layout_fun=layout_function)

with open('A4_dog.pdf', 'wb') as f:
    f.write(pdf)

Pretty simple right?!

However, the resultant file is fairly large, this is because our input image was large. In the next section, we'll use Pillow to reduce the file size then create the PDF.

Process and Convert an Image to PDF using Pillow

Pillow is a popular and powerful Python image processing library. We'll use it to process then convert an image to PDF.

First, install Pillow to our virtualenv

pip install Pillow

Let's open the Python shell and see what we can learn about our image. Type in the commands below:

from PIL import Image
dog_jpg = Image.open('<<name of the image file>>')    

This will load our image into memory and create an Image instance of it. We can find out some useful information about that image:

dog_jpg.format   # returns the image format
# output: 'JPEG'
dog_jpg.size   # returns the size of the image as (x pixels, y pixels)
# output: (3487, 5230)
dog_jpg.mode   # returns the image bands
# output: 'RGB'
dog_jpg.show()   # opens the image in your default viewer

Our image is fairly large, at 3MB. We can reduce the file size in two ways:

  1. Reduce the number of pixels
  2. Compressing the image more

Both methods will reduce the file quality and therefore size. I find a combination of both methods yields the best results.

Let's reduce the number of pixels and reduce the quality to 85%. Compression is not available for all image formats, refer to the Pillow docs for what is possible for each format.

For our file, we want it to fit into an A4 page (8.3 x 11.7 inches) with a resolution of 200 pixels per inch (PPI or dpi). Let's work out the number of pixels the entire page should contain:

page_size = [int(8.3 * 200), int(11.7 * 200)]
page_size
# output: [1660, 2340]

Our image is of course too large to fit into the page at this resolution. We need to resize it to make sure it fits. To do this, we'll scale down the width and height by the same factor so we retain the image aspect ratio (ratio of width to height) otherwise it'll look wonky.

target_width, target_height = page_size
current_width, current_height = dog_jpg.size
scale = max(current_width / target_width, current_height / target_height)

We set the scale as the larger of the width or height scale factor to ensure the image will certainly fit both dimensions of the page.

Divide both dimensions of the dog image by the scale to get the final size. We round it down because size values must be integers.

final_image_size = [round(current_width / scale), round(current_height / scale)]
final_image_size
# output: [1560, 2340]

Resize the image to its new dimensions. We'll resample the image using a filter called LANCZOS that yields the best quality but is slow. Since we're only processing one image, this shouldn't be a problem. You can read more about Pillow filters here.

This will make a new resized copy of the dog_jpg with the original instance remaining unchanged.

resized_image = dog_jpg.resize(size=final_image_size, resample=Image.LANCZOS)
resized_image.size
# output: (1560, 2340)

At this point, our image can fit into an A4 page. However, there will be 100 px on the width of the page left over. We need to fill this up so our image precisely fits an A4 page.

To do this, we'll create a new image that is all white with the dimensions of the A4 page then paste our dog image on top of it.

canvas_image = Image.new(mode='RGB', size=page_size, color='white')
canvas_image.show()

This should open a blank white image the size of an A4 page with a resolution of 200 PPI.

We want the image to be centred on the page, so we'll put 50px of whitespace on either side of the image.

Pillow uses a coordinate system to map images from the top left. For instance (220, 0) refers to 220px to the right from the top left of the image. (100, 50) refers to a point 100px from the left and 50px from the top of the image.

We want to paste our image along the top edge 50px to the right of the top left corner of the canvas. This translates to the position (50, 0).

Please note that the paste function mutates the image being pasted into. Make a copy of the image (canvas_image in our case) if this is not desirable.

canvas_image.paste(resized_image, box=(50,0))
canvas_image.show()

Let's save this image as a JPEG first before we convert it to PDF. The quality argument below refers to the amount of compression to apply to the image. Refer to the JPEG writer's documentation.

canvas_image.save('A4_dog_image.jpeg', format='JPEG', quality=85, optimize=True)

This will save the new image in the same directory as the virtualenv we created earlier. It should look like this:

The original image was 3MB and this final image came down to about 533KB and it is still usable.

Finally, let's convert this image to a PDF. The quality argument below in the context of the PDF write refers to the resolution of the resultant PDF in dpi/PPI. We'll pass in the 200 value we used to scale the images from the start.

canvas_image.save('A4_dog.pdf', format='PDF', quality=200)

And we're done! The resultant PDF will be significantly smaller than the initial one we generated because we scaled-down and compressed the image.

Conclusion

Pillow is a powerful library that supports reading from, processing and writing of many more file formats to both raster and vector images. Check out the Pillow documentation on file formats. We've barely scratched the surface of what Pillow can do.

These solutions aren't mutually exclusive, you can use Pillow to process the image(s) then use img2pdf to create the PDF files.

Both these packages also allow the creation of multiple page PDFs from multiple images. Read through the respective docs to learn how to do this. It'll however follow a similar process to what we've done here.