ImageMagick: Convert image to PDF without losing quality

If you’re a nerd like me, you probably like using ImageMagick’s convert command line tool to manipulate images or convert them to PDF. It’s fast, powerful and is able to work on tons of images at a time. You could probably do the same in Photoshop or some Adobe product, but come on… what’s the fun in that!

But with great power comes great responsibility… and great confusion. How many times have you had to trawl through Stackoverflow just to use this command properly? I’ve spent hours – days, even – trying out the 290 (!) different options, just to find the combination that works.

Something I do quite regularly is to convert images to PDF. To make them look nice, I’ll also add some margins and centre them on the page. Not too complicated, right?

It turns out that doing this in ImageMagick without losing image quality involves quite a bit of (pun not intended) magic. You’ll need to set the page size, calculate a density, and use an offset to centre the image manually. Frustratingly, there isn’t a simple way to centre an image.

Just as you’d expect of a nerd, I’ve created a script to do this for me. Behold, the imaginatively-named img_in_pdf script:


#!/bin/bash
# Converts input images to one-page PDF files each, without changing image data.
# The image is centered on a A4 page with a 5% border.
# Adapted from https://unix.stackexchange.com/a/220114
#
# Usage: [command] image1.jpg image2.png ...
# Output: PDF files named after the images e.g. image1.pdf

# bc function to calculate maximum of two floats
bc_functions="
define max(a,b) {
  if (a>b) {
    return(a)
  } else {
    return(b)
  }
};";

# Do the calculation in string $1 and echo the result.
function calc {
  # Define bc functions so we can use it for the calc.
  echo "$bc_functions $1" | bc -l;
}

for file in "$@"; do \
  # Determine image dimensions in pixels.
  img_size_x=$(identify -format "%w" "$file");
  img_size_y=$(identify -format "%h" "$file");

  # Calculate image density (in dpi) needed to fit the image and a 5% 
  # border all around on an A4 page (8.27x11.69"). Factor 1.1 creates 
  # 2*5% borders, see https://unix.stackexchange.com/a/220114 for details.
  min_density_x=$(calc "$img_size_x / 8.27 * 1.1");
  min_density_y=$(calc "$img_size_y / 11.69 * 1.1");
  # Use the higher density to prevent any dimension exceeding the required fit.
  density=$(calc "max($min_density_x,$min_density_y)");

  # Calculate canvas dimensions in pixels.
  # (Canvas is an A4 page (8.27x11.69") with the calculated density.)
  page_size_x=$(calc "8.27 * $density");
  page_size_y=$(calc "11.69 * $density");

  offset_x=$(calc "($page_size_x - $img_size_x) / 2 * 72 / $density");
  offset_y=$(calc "($page_size_y - $img_size_y) / 2 * 72 / $density");

  # Center image on a larger canvas (with a size given by "-extent").
  convert "$file" \
    -page ${page_size_x}x${page_size_y}+${offset_x}+${offset_y} \
    -units PixelsPerInch -density $density \
    -format pdf -compress jpeg \
    "${file/.jpg/.pdf}";
done;

 

What this script does, quite simply, is:

  • Get the dimensions of the image.
  • Calculate the resulting page dimensions if we add 5% margins to all sides of the image.
  • Determine the density (i.e. how many dots per inch or DPI) the image should be set to, so that it would fit into an A4 canvas with the required margins.
  • Use the density to calculate the page size in pixels.
  • Use the density to calculate the image offset, i.e. how far from the bottom left corner it should be, so that the image would  look “centred”. In this sense, the PDF rendering mechanism doesn’t actually centre it. It just displaces it so it looks centred.
  • Run the convert command with the calculated parameters to generate a PDF from the image.

Mathematical calculations are done using the Linux bc tool.

To use the script, download it from the Github repository and make it executable using chmod u+x. You can then execute it like this:

./img_in_pdf.sh image1.jpg image2.png ...

Hope you find this useful!

Leave a Reply

Your email address will not be published. Required fields are marked *