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:
- Reduce the number of pixels
- 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.