Snell scripting
November 1, 2014 at 9:03 AM by Dr. Drang
Given that I know nothing about delivering Retina images on the web, it’s presumptuous of me to offer my own version of the script/Automator service Jason Snell posted the other day. But he kind of challenged me, so here it is—after a couple of brief diversions.
First, there’s no reason for Jason to use my script. His works fine, and, what’s more important, it uses tools he’s comfortable with. Quick, purpose-built scripts like this can break because of otherwise benign changes to the environment in which they run; and when they do, you have to be able to fix them. More commonly, you’ll get a better idea of how to do things and the script has to change to accommodate your new workflow. Either way, it does you no good to depend on a clever script written a language you don’t understand.1
This doesn’t mean I’m against learning new things. Studying Jason’s script, for example, showed me some features of scripting Acorn and Transmit that I’d never seen. And ANIAT is filled with bits of code that I’ve just learned or adapted from elsewhere. But these new things are usually just small steps away from what I already know—it’s the accumulation of small steps over time that turn them into significant new skills. Over the past month or so, for example, I’ve built my own static blogging system, something that would have seemed inconceivable to me when I started blogging a decade ago. And yet it didn’t really seem that hard, because it was done by combining and adjusting dozens of little helper scripts I’d written over the years.2
Second diversion: Jason’s thumbnail description of the AppleScript used in his Automator service is
The script loops through every file selected in the Finder, opens them in Acorn, resizes them to the two sizes (or their equivalent sizes if they’re vertical rather than horizontal images) and saves out JPEG files, then uploads them via Transmit. It’s quick and dirty—if anything in the file’s name or path contains a period (other than the filename itself), it won’t work. And I’m sure that Dr. Drang would use the command-line tool
sips
instead.
My ill-considered response to this was
@jsnell Best thing is use Image Events: macosxautomation.com/applescript/im…
Same features as sips, but intended for AppleScript instead of the shell.
— Dr. Drang (@drdrang) Oct 30 2014 11:06 PM
I said this because both the man page for sips
and what I’ve read about Image Events say that the AppleScript commands available through Image Events are equivalent to what you can do in the Terminal with sips
. But when I started writing an AppleScript using Image Events to replicate—and, I hoped, simplify—Jason’s script, I learned that wasn’t true.
Here’s the description of the scale command from the Image Events dictionary:
scale v : Scale an image
scale specifier : the object for the command
[by factor real] : scale using a scalefactor
[to size integer] : scale using a max width/length
To me, this looks like you can’t set the width or the height specifically, only the larger of the two. Now there may be undocumented options you can give to scale to specify which dimension you want to set the size of, but even if there are, I’m not a fan of undocumented language features and would prefer to avoid them.
The sips
utility is much more flexible. It has the resampleHeightWidthMax
option, which is equivalent to AppleScript’s scale … to size, but it also has resampleWidth
and resampleHeight
, which let you set either dimension and have the other scale proportionately, and it has resampleHeightWidth
, which lets you set both dimensions and, potentially, distort the image.
In short, Jason was right: I prefer using sips
.
Here, then, is the script I came up with that mimics Jason’s:
bash:
1: #!/bin/bash
2:
3: for f in "$@"; do
4: # Set up the new file names.
5: base="${f%.*}"
6: x1="$base-6c.jpg"
7: x2="$base-6c@2.jpg"
8:
9: # Get the width and height of the image.
10: w=`sips -g pixelWidth "$f" | awk 'NR==2 {print $2}'`
11: h=`sips -g pixelHeight "$f" | awk 'NR==2 {print $2}'`
12:
13: # Resize the images based on the width-height relationship.
14: if (( w > h + 20 )); then
15: sips -s format jpeg --resampleWidth 680 "$f" --out "$x1"
16: sips -s format jpeg --resampleWidth 1360 "$f" --out "$x2"
17: else
18: sips -s format jpeg --resampleWidth 340 "$f" --out "$x1"
19: sips -s format jpeg --resampleWidth 680 "$f" --out "$x2"
20: fi
21:
22: # Upload the images.
23: scp -P 9922 "$x1" "$x2" jason@sixcolors.com:path/to/images/
24: done
The advantage for me is that it uses tools I understand and doesn’t launch any GUI applications that I’ll have to quit by hand later. The potential advantage for others is that they’ll learn a few new shell tricks (as I did while writing it).
The script assumes that the arguments to the script are the files that are going to be resized. The overall structure of the script is a loop through that list of files bounded by Lines 3 and 24.
Lines 5–7 generate the names for the resized files. Line 5 uses a trick I first learned in Cameron Newham and Bill Rosenblatt’s Learning the bash Shell. The %
in the curly braces after the file name variable, f
, deletes the shortest part of f
that matches .*
and returns the rest. The enclosing double quotes makes sure that any spaces or shell-command characters in the file name are preserved. Basically, what this does is strip off the .jpg
or .png
or .tiff
from the file name. It’s tiny bit better than Line 7 of Jason’s AppleScript, in that it allows periods elsewhere in the file/path name.
As an example, if the file name
~/Dropbox/blog.images/BBEdit Languages Preferences.png
is passed to the script, base
will become
~/Dropbox/blog.images/BBEdit Languages Preferences
and will be properly quoted so the rest of the script doesn’t see it as three separate strings.
Lines 6 and 7 create the filenames for the resized images by adding -6c.jpg
and -6c@2.jpg
to the end of base
. Again, the double quotes preserve spaces and other special characters.
If you know something about the shell basename
command, you might be tempted to use it here. Don’t. While it’s true that basename
can strip off a file’s extension, it can only do so if you know the extension ahead of time. Because this script is intended to be used for any type of image file, we can’t say what the extension will be.
Notice that I’ve just spent four paragraphs describing three short lines of code. That’s both the blessing and the curse of shell scripting. A lot can be done with just a few keystrokes, but what the keystrokes do is anything but obvious. And don’t get me started on the rules about whitespace.
Lines 10–11 use sips
and its -g
(getProperty
) option to pull out the width and height of the original image. Unfortunately, sips
, like many OS X command-line utilities, is more verbose than it ought to be. Instead of just returning the width, sips -g pixelWidth file.png
returns something like
/Users/drdrang/Desktop/file.png
pixelWidth: 917
where the first line is the full path name and the second has both the property name and its value. To strip away all the junk and get just the width, I had to pipe the output through awk
to get just the second field (print $2
) of the second line (NR==2
). The pipelines are surrounded by backticks to get their output, which are then assigned to w
and h
.
With the width and height of the image in hand, we now set up a conditional that checks the relationship between the two before creating the resized images. The double parenthesis construct in Line 14 allows integer arithmetic to be done within its confines. Jason wants the image to be resized one way if the width is more than 20 pixels greater than the height and another way if it’s not.
Lines 15–16 or 18–19 actually do the resizing, and here, finally, are some lines whose purpose is fairly obvious. Jason wants the resized files to be of a certain width—with the Retina (@2
) image naturally twice the width of the non-Retina image—and to be saved as JPEGs, regardless of the original image format.
Line 23 uploads the resized images to a particular directory on the server using a secure, ssh
-powered connection through the scp
command. There’s a bit of a trick to this, which I’ll get to in a minute, but first let’s talk about the structure of the scp
command.
I’ve added the possibly unnecessary and certainly incorrect -P 9922
option because some servers use a non-standard port number for ssh
connections. The standard port number for ssh
is 22, and if your server uses that, there’s no need for any -P
option. But many web hosting companies use another number to cut down on the amount of malicious traffic trying to get shell access. If that’s how the sixcolors.com server is set up, Jason will have to use the -P
option and provide it with the proper port number.
The first set of (non-option) arguments to scp
are the paths to the source files, which in our case are the x1
or x2
files, quoted, as always, to preserve spaces and other special characters. The final argument is the destination, which includes the username, the domain, and the path. Apart from the domain, all of this is made up—I have no idea what Jason’s username is, nor do I know where he keeps his images on the server.3 By giving the destination as a directory name (the trailing slash ensures that), the uploaded file will be given the same name as the source file.
Update 11/1/14
Initially, I had two scp
lines, one for x1
and one for x2
. As pointed out by Aristotle Pagaltzis, this is a waste of both code and connection overhead, so I merged the two uploads into a single command.
@drdrang: You can copy the Retina and non-Retina images with a single scp command. Saves the overhead of setting up a new connection.
— Aristotle (@apag) Nov 1 2014 4:10 PM
If I start using this command as part of my workflow, I’ll probably save the resized files into their own special local directory in Lines 15–16 and 18–19 and use rsync
at the tail end of the script (after the loop) to upload them to a similar directory on the server. This would mean just one connection to the server no matter how many files were resized.
Now comes the tricky part: explaining how you can call scp
without it asking for either your password on the server or your ssh
passphrase. To avoid screwing it up—and making this post even longer—I’m going to pass you off to other articles on this topic. The clearest explanation I’ve found comes from a combination of Steps 1–3 from this article at Digital Ocean and this answer to a StackExchange question. When your ssh keys are set up and your passphrase is securely stored in the OS X Keychain, you can run scp
from the command line or within a script without entering a password or passphrase.
OK, so now we have a script that will do the resizing and uploading from the command line. It can easily be turned into a Service via Automator:
The Service takes image files selected in the Finder. The only action is Run Shell Script with the script above (sans #!
line) and the input set to “as arguments.” Once saved, this service will be available from the right-click menu when you have one or more image files selected in the Finder.
I doubt Jason will feel more comfortable with this script than with the one he built himself, and that’s as it should be, but people who want to serve Retina images and don’t use Acorn and Transmit may find it helpful. At the moment, I have no use for this script because my images are stored on Flickr and I’m not using Retina. But when I switch to a VPS next year, I intend to stop using Flickr as a poor man’s CDN and store my images on the VPS itself. This script will be the basis of my new workflow.
-
This is why I generally don’t use Brett Terpstra’s scripts. He writes in Ruby, and while I can read Ruby well enough, I have no confidence in my ability to write it. ↩
-
It’s still not in a state where I’m ready to show it to the world—right now, I’m probably the only person who could use it. But it’s steadily getting better, and publishing a post is now almost as easy for me with the new system as it was using WordPress. ↩
-
Yes, I can see that the path to the image directory ends with
images/content/
, but I don’t know what precedes it (although I could probably guess it and so could you). ↩