Yesterday, I listened to the most recent Mac Power Users episode, “20 Mac Apps Under $20.” Although MPU is probably best known for its deep dive episodes, I always like these more rapid-fire discussions. As usual, this one has a good mix of apps that are new to me and those I already know about.1 One of Stephen’s picks is ImageOptim (discussion starts at 1:00:50), an app I use all the time but haven’t talked about here.

As suggested by its name, ImageOptim optimizes images by reducing their file size. It doesn’t resize an image in the sense of changing its width or height in pixels. What ImageOptim leaves you with is an image that takes up less space on your disk; it doesn’t take up less space on your screen.

I’m sure you already know that a JPEG’s file size can be reduced by lowering its “quality.” ImageOptim can do this kind of optimization, but so can lots of apps. what ImageOptim really excels at is reducing the file size without changing the image quality. And it can do this for PNGs, which is the format I use for the screenshots I post here. Generally, I get 25–40% smaller files after running them through ImageOptim.

Fundamentally, ImageOptim is a front end for a set of open source image optimization programs. You can see and choose the programs in either the Preferences or the Tools menu.

ImageOptim Tools menu

The given image is run through each of the checked programs appropriate for its file type, and the one that provides the smallest file is selected. The image file is overwritten with that optimized version and you’re told how much smaller the file became. You’ll note that I don’t have my images run through Zopfli or PNGOUT, as these programs run slower than the others, and I don’t think the extra time is worth slight extra reduction in file size.

This is all very well and good, but I wouldn’t think as highly of ImageOptim if it weren’t for one very useful feature: it can be run as a command line program. Which means that my Keyboard Maestro macro for taking screenshots and uploading them to the blog’s server can reduce their file size during the process.

(I used to use OptiPNG for this, but at some point a couple of years ago it started running very slowly, and the delay was disrupting even the slow pace of my writing. I just ran a few tests, and OptiPNG seems to be running fine on my Macs now, but I’m sticking with ImageOptim.)

My screenshot-and-upload macro, SnapSCP, looks like this:

SnapSCP KM macro

The Python script in Step 1 is this:

 1:  #!/usr/bin/env python3
 3:  import Pashua
 4:  import tempfile
 5:  from PIL import Image
 6:  import sys, os, os.path
 7:  import subprocess
 8:  import urllib.parse
 9:  from datetime import date
10:  import sys
12:  # Parameters
13:  dstring ='%Y%m%d')
14:  year ='%Y')
15:  type = "png"
16:  localdir = os.environ['HOME'] + "/Pictures/Screenshots"
17:  tf, tfname = tempfile.mkstemp(suffix='.'+type, dir=localdir)
18:  bgcolor = (61, 101, 156)
19:  border = 16
20:  optimizer = '/Applications/'
21:  server = f'{year}/'
22:  port = '9876'
24:  # Dialog box configuration
25:  conf = b'''
26:  # Window properties
27:  *.title = Snapshot
29:  # File name text field properties
30:  fn.type = textfield
31:  fn.default = Snapshot
32:  fn.width = 264
33:  fn.x = 50
34:  fn.y = 40
35:  fnl.type = text
36:  fnl.default = Name:
37:  fnl.x = 0
38:  fnl.y = 42
40:  # Border checkbox properties
41:  bd.type = checkbox
42:  bd.label = Add background
43:  bd.x = 10
44:  bd.y = 0
46:  # Default button
47:  db.type = defaultbutton
48:  db.label = Save
50:  # Cancel button
51:  cb.type = cancelbutton
52:  '''
54:  # Capture a portion of the screen and save it to a temporary file.
55:  status =["screencapture", "-io", "-t", type, tfname])
57:  # Exit if the user canceled the screencapture.
58:  if not status.returncode == 0:
59:    os.remove(tfname)
60:    print("Canceled")
61:    sys.exit()
63:  # Open the dialog box and get the input.
64:  dialog =
65:  if dialog['cb'] == '1':
66:    os.remove(tfname)
67:    print("Canceled")
68:    sys.exit()
70:  # Add a desktop background border if asked for.
71:  snap =
72:  if dialog['bd'] == '1':
73:    # Make a solid-colored background bigger than the screenshot.
74:    snapsize = tuple([ x + 2*border for x in snap.size ])
75:    bg ='RGBA', snapsize, bgcolor)
76:    bg.alpha_composite(snap, dest= (border, border))
79:  # Rename the temporary file using today's date (yyyymmdd) and the
80:  # name provided by the user.
81:  name = dialog['fn'].strip()
82:  fname =  f'{localdir}/{dstring}-{name}.{type}'
83:  os.rename(tfname, fname)
84:  bname = os.path.basename(fname)
86:  # Optimize the PNG.
87:[optimizer, fname], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
89:  # Upload the file via scp.
90:['scp', '-P', port, fname, server])
92:  # Generate the URL of the uploaded image.
93:  bname = urllib.parse.quote(bname)
94:  print(f'{year}/{bname}', end='')

How this script uses Pashua, screencapture, and scp are described in an older post. The only thing that’s different now is the use of ImageOptim. The full path to the executable is given in Line 20,

optimizer = '/Applications/'

and it’s run via the subprocess library in Line 87,

python:[optimizer, fname], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

When the optimized screenshot is done uploading and its URL is on the clipboard, the macro plays the Glass sound to alert me that I can paste the URL wherever I need it. It typically takes 3–5 seconds.

So ImageOptim gives you the best of both worlds: a nice app for optimizing images through the GUI and a command-line tool for doing the same thing in scripts and macros. It’s even better than Stephen said it was.

  1. What’s good about a discussion of apps I’m familiar with? Well, I’m usually not as familiar with them as I think. There’s often a feature or two I never knew about. 

Feet, inches, and averaging

After Purdue’s historically embarrassing loss to Fairleigh Dickinson on Friday night, many sportswriters awakened to Purdue’s longstanding poor performance in the NCAA tournament and wrote articles like this one in Slate. As I read that article, I saw one error that should embarrass Slate and then saw another statement that seemed like an error but wasn’t. I figured they were worth a short blog post.

The embarrassing error concerns the height difference between Purdue and FDU. Purdue’s top player, Zach Edey, is 7′ 4″. According to Slate, FDU has “nobody taller than 6-foot-6.” Slate apparently doesn’t know how to use foot and inch marks, but I don’t consider that an error. And even though a quick perusal of FDU’s roster clearly shows a 6′ 7″ player, I’m going to give them a pass on that, too, as Pier-Olivier Racine doesn’t get much court time and didn’t play at all in the Purdue game. No, the error comes later when, after establishing that Edey is 7′ 4″ and the tallest players on FDU are 6′ 6″, the article says

Edey is 8 inches taller than anyone who guarded him.

My guess is that the writer, Alex Kirshner, did the calculation in his head this way,

74 – 66 = 8

and everyone at Slate who read the article before publication just went along with it. I’m also pretty sure that all my non-American readers are nodding their heads and congratulating themselves on not using those awful and confusing US customary units. But honestly, is it really that hard to see that 7′ 4″ is actually 10″ taller than 6′ 6″? I bet even Purdue’s engineering students would get that one right.

Here’s the thing that seemed like a mistake but wasn’t:

There are 363 Division I men’s basketball teams. FDU is the shortest one of them all, according to Ken Pomeroy’s data, with an average height of 6-foot-1.

This wasn’t the first time I’d heard that Fairleigh Dickinson’s team had an average height of 6′ 1″—it’s in several of the Purdue-excoriating articles. And it wasn’t the first time I thought that can’t be right. I don’t need KenPom to tell me the team’s average height (and a good thing, too, because it’s behind a paywall), all the heights are on the afore-linked roster.

Scrolling through the list of players, there are only two under 6′ 1″, one who is 6′ 1″, and ten others who are taller. Even without doing a single calculation, I knew the average had to be over 6′ 1″. And it is. To the nearest half-inch,1 the average height of the thirteen players on FDU’s roster is 6′ 3″.

The thing is, while I can believe that Slate would make a mistake in calculating a team’s average height, I can’t believe Ken Pomeroy would. So how did he come up with 6′ 1″? The answer is on his site, and it’s a good one:

Overall average height is computed by taking the average listed height of every player on the team, weighted by minutes played. Players that have played less than 10% of their team’s minutes are not included.

Pomeroy wants to give his readers the average height of the team on the court, and weighting the players’ heights by minutes played is probably the best way to do that. And dropping out those who play in “garbage time” makes sense, too.2

So I made a little spreadsheet with the FDU players, their heights, and the minutes they played in the Purdue game. Here’s a screenshot:

FDU height spreadsheet

As you can see, the team’s shorter players, Roberts and Singleton, also got the most minutes, so the time-weighted average, rounded to the nearest half-inch, for this particular game was 6′ 1½″. Close enough. I wouldn’t be surprised to learn that FDU had its taller players on the court more than usual in this game.

The spreadsheet also includes the naive average of 6′ 3″ and the 6′ 2″ average of the nine FDU players who played in the Purdue game.

By the way, I didn’t enter the heights in feet and inches as shown in the second column. I entered the heights in inches in the third column and converted to feet and inches for presentation with this formula for the player heights

FIXED(FLOOR(C2÷12,1), 0) & "′ " &
FIXED(MOD(C2, 12), 0) & "″"

and this formula for the average heights.

FIXED(FLOOR(C16÷12,1), 0) & "′ " &
FIXED(MROUND(MOD(C16, 12), 0.5), 1) & "″"

I needed the MROUND function in the latter formula to get the results rounded to the nearest half-inch.

The “played average” was calculated this way:


I’m not a big spreadsheet user, and I think this is the first time I ever used MROUND or AVERAGEIF. So thanks to Slate and KenPom for that.

  1. The Slate article says Purdue’s average height is 6′ 5½″, so I assume KenPom gives average heights with that resolution or finer. 

  2. One might argue that someone who averages 4 minutes per game is getting more than garbage time, but the principle is sound. 

Better MathJax equations

If some of the equations in my posts look a little off—poorly spaced or, more likely, with a wobbly baseline—there may be something you can do about it. I learned this on Mastodon through a combination of Andy Napper and Mimmo.

To some extent, the formatting of equations depends on the browser you use, but I’m not going to tell you switch from your favorite browser just to read this blog. Instead, you can change how MathJax renders equations in whatever browser you use by tweaking a setting or two.

Here’s an example of an uneven baseline from a recent post:

MathJax equation rendered with CHTML

This is how Safari renders the equation when MathJax is using the “CHTML” (CSS and HTML) renderer. But if you right-click on the equation and work your way through the MathJax settings, you can switch to the SVG renderer

MathJax renderer setting

to get a more even baseline

MathJax equation rendered with SVG

The nice thing about this is that even though you change the setting on one equation, it applies to all the equations. Even better, it’s persistent—you don’t have to keep changing it every time you open a different post with equations.

If you find yourself zooming in on equations often, you might want to set up a “Zooom Trigger” to show a magnified view of any equation you click on:

MathJax zoom trigger setting

The problem with the Zoom Trigger is that it shows the magnified equation in a box that might not be big enough to show the whole equation at once:

MathJax magnified equation in a box

Even though you can scroll around within the box to see the rest of the equation, I never use this feature. I find it much better to zoom the entire web page by either spreading two fingers on the trackpad1 or pressing ⌘+ a few times.

It’s been a long time since I reviewed ANIAT’s MathJax configuration, and I’m sure there are things I can do to improve the rendering for everyone. Until I get around to that, have a look through all of MathJax’s settings to see if any of its customizations strike your fancy.

  1. I refuse to call this “pinching,” no matter what Apple and everyone else does. 

Concrete and pi

Back in either 1980 or ’81, I took course called Properties and Behavior of Concrete, which had the course number CE 214 and this description:

Engineering properties of plain concrete; influence of cement, aggregates, water, and admixtures on the properties of fresh and hardened concrete; microstructure of cement paste and concrete; mix design; handling of fresh concrete; and behavior under various types of loading and environments. Laboratory practice is an important part of the course.

The University of Illinois’s civil engineering department has undergone course number inflation over the past 40 years. That course is now CEE 401, but the description is nearly identical:

Examination of the influence of constituent materials (cements, water, aggregates and admixtures) on the properties of fresh and hardened concrete, concrete mix design, handling and placement of concrete, and behavior of concrete under various types of loading and environment. Laboratory exercises utilize standard concrete test methods. Field trips are held during some scheduled laboratory sessions.

When I took the course, it was taught by J. Francis Young, whose book on concrete was still being copyedited (we students got photocopies of proofs with his handwritten corrections as handouts). Friends of mine who took it the following semester were taught by Clyde Kesler, the father of the concrete canoe, shortly before he retired.

The differences between Young and Kesler were striking. Kesler was a local boy, born and raised within a few miles of the university he got his degrees from and then spent his entire career at. Young was born in New Zealand, got his Ph.D. in London, and then had a long career in the middle of Illinois. They were brought together by concrete and academics.

What does this have to do with pi? During the part of the class on mix design, we learned how to proportion the water, cement, large aggregate (gravel), small aggregate (sand), and admixtures to get certain target properties in the concrete. During the mix design process, you do some calculations using the specific gravities of these constituents.

The specific gravity of portland cement is generally taken to be 3.15, but it’s not an exact figure. Prof. Young told us to just tap the π button on our calculators when we needed to enter it. This tip didn’t save us much time, but it was a great way to teach us the specific gravity of cement. In the 40+ years since taking that class, I’ve had to go through the mix design process only a few times, but every time I remembered to press the π button.