Tuesday, January 25, 2022

Fighting Finicky Frames; Perfectly Printing Pics Programmatically

A few months ago, Shira picked up a couple of these photo-cubes with the plans to turn one into a gift:

Each side of the cube holds a 2.5x2.5 inch photo with only 2x2 inches visible. The challenge became: how can I prepare a series of pictures such that when they are printed, they will output as 2x2 inch photos on 4x6 inch paper.

Let's Do Some Math

Here was my approach: crop the pictures that Shira liked into squares. For the sake of an example, assume that each of the pictures are 1382x1382 pixels. Using a little algebra, I solved for the pixel width and height of the completed 4x6 photo:

# Derive the width
2 inches         6 inches
--------     =  ----------
1382 pixels      w pixels

2w = 6*1382
w = (6*1382) / 2
w = 4146


# Derive the height
2 inches         4 inches
--------     =  ----------
1382 pixels      h pixels

2h = 4*1382
h = (4*1382) / 2
h = 2764

Within Gimp, I resized the canvas of each of the photos to the calculated dimensions (4146x2764). We then sent off the pics to be printed and cut away the whitespace until the images fit the fame. The final product looked great.

Since then, I've been staring at the backup photo cube Shira bought and thinking: I really need to automate this process and try printing the photos again.

Along the way, while visiting the kids in Florida, G showed me a locket that she'd 'borrowed' from her Mom. It was pretty, but was missing the photo inside. I took pics of its dimensions and vowed I'd get her a photo that would fit. I realized, this was just another version of the photo-cube problem:

Let's Code This

I pondered a few different solutions to this image manipulation problem and arrived at a simple approach. My plan was to write a shell script that used ImageMagick to do the following:

  1. Calculate the requested aspect ratio of both the final size (2x2 inches) and the provided photo (1382x1382), and confirm that they match.
  2. Calculate the conversion factor: 1 inch = x pixels based on the pixel size of the input photo and the requested output size.
  3. Use ImageMagick's convert tool's -border option to pad the image with extra space.

For example, here's the command I used to convert a directory full of square photos for output as 2x2 inch photos printed on 4x6 inch paper. The -b option added a .25 inch border around the photo, which makes it easier to trim. This .25 inch border means that each photo is now 2.5x2.5 inches, the frame's advertised size.

$ for f in inputs/*.jpg; do echo $f ; picassist -a prepare-print -W 6 -H 4 -w 2 -h 2 -b .25 -f $f -o outputs/$(basename $f) ; done
inputs/20190715_130759.jpg
inputs/20190716_073107.jpg
inputs/20210802_071223.jpg
inputs/20210803_102944.jpg
inputs/20210805_191843.jpg
inputs/f_img_4832.jpg

And here's the same command, but now I'm preparing the images for printing on 8.5x11 inch printer paper at home. Note that I'm still generated 2x2 inch photos with a .5 inch border.

for f in scaled/*.jpg; do echo $f ; picassist -a prepare-print -W 8.5 -H 11 -w 2 -h 2 -b .25 -f $f -o 8.5x11/$(basename $f) ; done
scaled/20190715_130759.jpg
scaled/20190716_073107.jpg
scaled/20210802_071223.jpg
scaled/20210803_102944.jpg
scaled/20210805_191843.jpg
scaled/f_img_4832.jpg

I used the same strategy for making prints for the locket. As far I can tell, the dimensions of the locket are 0.6875x0.9375 inches. I added a 1/16" border around them:

 picassist -a prepare-print -W 4 -H 6 -w 0.6875 -h 0.9375 -b 0.0625 -f scaled.jpg -o output.jpg

Picture Perfect. Almost.

I sent a series of photos to CVS to be printed. When I picked them up a few hours later I was disappointed to see that the images were about 2/16" off. Something must be getting scaled unexpectedly in the process. I printed the photos on my laser printer, and found similar results. Both the gray border and picture are a couple of 16ths off.

Undeterred, I went to work trimming the photos and sliding them into the cube. Even with the dimensions being slightly off, the photos do look good:

I'm going to call this a success. I was able to prepare the photos, send them off to a printer and trim them with minimal effort. Wacky frame sizes, I no longer fear you!

Below is the script that does the image tweaking. Enjoy!


#!/bin/bash

##
## Do useful stuff with photos
##

usage() {
  me=$(basename $0)
  echo "Usage: $me -a prepare-print -W outer-width -H outer-height -w inner-width -h inner-height -b border-width -f img.jpg -o out.jpg"
  echo ""
  echo "Ex: $me -a prepare-print -W 6 -H 4 -w 1.5 -h 1.5 -b .2 -f img.jpg -o final.jpg"
  echo "  (print a 1.5x1.5 inch photo on a 4x6 in print)"
  exit 1
}

action=""

while getopts "a:W:H:w:h:b:f:o:" o; do
  case "$o" in
    a) action=$OPTARG ;;
    W) outer_width=$OPTARG ;;
    H) outer_height=$OPTARG ;;
    w) inner_width=$OPTARG ;;
    h) inner_height=$OPTARG ;;
    b) border_width=$OPTARG ;;
    f) file=$OPTARG ;;
    o) output=$OPTARG ;;
    *|h) usage ;;
  esac
done

calc() {
  echo "scale=3 ; $* " | bc -l
}

im_id() {
  identify "$@"
}

case "$action" in
  prepare-print)
    if [ -z "$outer_width" -o -z "$outer_height" -o -z "$inner_width" -o -z "$inner_height" -o -z "$border_width" ] ; then
      echo "Missing W, H, w, h or b."
      exit 1
    fi

    if [ -z "$file" ] ; then
      echo "Missing -f file"
      exit 2
    fi

    if [ ! -f "$file" ] ; then
      echo "File [$file] doesn't exist"
      exit 3
    fi

    if [ -z "$output" ] ; then
      echo "No output file set."
      exit 4
    fi
    
    real_width=$(im_id -format '%w' $file)
    real_height=$(im_id -format '%h' $file)
    
    required_ratio=$(calc "$inner_width / $inner_height")
    real_ratio=$(calc "$real_width / $real_height")

    if [ "$required_ratio" != "$real_ratio"  ] ; then
      echo -n "Ratio mismatch: require ${inner_width}x${inner_height} (${required_ratio}) != "
      echo    "${real_width}x${real_height} (${real_ratio})"
      exit
    fi

    px_per_real=$(calc "$real_width / $inner_width")

    border_px=$(calc "$border_width * $px_per_real")

    h_margin=$(calc "(($outer_width - $inner_width - ( $border_width * 2)) * $px_per_real) / 2")
    v_margin=$(calc "(($outer_height - $inner_height - ( $border_width * 2)) * $px_per_real) / 2")

    convert  $file \
             -bordercolor gray -border $border_px \
             -bordercolor white -border ${h_margin}x${v_margin} \
             $output
    ;;

  *) usage ;; 
esac

No comments:

Post a Comment