Images, the clipboard, and AppleScript

Last week, my SnapClip macro crapped out with an error message. In the process of fixing it, I learned something about AppleScript that probably won’t ever help me in the future.

SnapClip is a Keyboard Maestro macro that puts a graphical user interface on a Python script, process-screenshot. The problem that led to the error was a particular branch of process-screenshot: the branch that puts the screenshot onto the clipboard. I’m not going to rehash the design of SnapClip or process-screenshot here—you can read the old post about them if you want the details.

The branch of process-screenshot that failed runs the impbcopy command-line utility written by Alec Jacobson over a decade ago. I’ve had impbcopy running successfully on my Macs for many years, but last week it failed with the error message

Bad CPU type in executable

I assumed the problem was that impbcopy had been compiled back when I was using an Intel Mac, and it had continued to work because of Rosetta. I thought that because I had Rosetta installed before, it would be updated along with macOS 27, but it wasn’t. Running

softwareupdate --install-rosetta

as described on the Rosetta page got SnapClip working again.

But macOS 27 is the end of the road for Rosetta, so it seemed like a change was due. I installed the Xcode Command Line Tools and tried to recompile impbcopy for Apple Silicon, but that failed with error messages that I couldn’t interpret because I’m not an Objective-C programmer. So I had four options:

  1. Learn enough about gcc/Objective-C/clang to get the compilation to work. Even if I could do this, I assumed it would be a lot of work with no future value for me.
  2. See if I could rewrite the SnapClip macro to use Keyboard Maestro features to put the image on the clipboard. This was more appealing, but I really liked the way the macro was structured and preferred to leave it as a GUI wrapper.
  3. Look into using Shortcuts to put the image on the clipboard and—if it can—replace the call to impbcopy with an equivalent call to the shortcuts utility. But I hate Shortcuts and would rather avoid it if possible.
  4. Work out an AppleScript solution and put a call to osascript into process-screenshot. I knew that AppleScript has commands for reading files and putting things on the clipboard, so I went with this, even though AppleScript may be more of a dead end than Objective-C.

Apple’s online documentation for AppleScript sucks, but I have three old AppleScript books that collectively weigh over nine pounds:

The answer (more or less) was in Learn AppleScript and led to just a couple of changes to process-screenshot. Here’s the new version:

python:
 1:  #!/usr/bin/env python3
 2:  
 3:  import tempfile
 4:  from PIL import Image
 5:  import os
 6:  import subprocess
 7:  import shutil
 8:  from datetime import datetime
 9:  import urllib.parse
10:  import argparse
11:  
12:  # Handle the arguments
13:  desc = 'Process a screenshot saved to a file.'
14:  ep = '''If a title is given, it saves the image to a file on the
15:  Desktop with a filename of the form yyyymmdd-title.png.
16:  If no title is given, the image is put on the clipboard and the
17:  upload option is ignored.'''
18:  parser = argparse.ArgumentParser(description=desc, epilog=ep,
19:            formatter_class=argparse.RawDescriptionHelpFormatter)
20:  parser.add_argument('-b', '--background', help='add desktop background border', action='store_true')
21:  parser.add_argument('-u', '--upload', help='upload to images directory and print URL', action='store_true')
22:  parser.add_argument('-t', '--title', help='image title', type=str)
23:  parser.add_argument('ssfile', help='path to screenshot file')
24:  args = parser.parse_args()
25:  
26:  # Parameters
27:  type = "png"
28:  bgcolor = (85, 111, 137)
29:  border = 32
30:  optimizer = '/Applications/ImageOptim.app/Contents/MacOS/ImageOptim'
31:  ascript = f'set the clipboard to (read POSIX file "{args.ssfile}" as picture)'
32:  
33:  # Add a desktop background border if asked for
34:  if args.background:
35:    snap = Image.open(args.ssfile)
36:    # Make a solid-colored background bigger than the screenshot.
37:    snapsize = tuple([ x + 2*border for x in snap.size ])
38:    bg = Image.new('RGBA', snapsize, bgcolor)
39:    bg.alpha_composite(snap, dest=(border, border))
40:    bg.save(args.ssfile)
41:  
42:  # Optimize the file
43:  subprocess.run([optimizer, args.ssfile], stderr=subprocess.DEVNULL)
44:  
45:  # Save it to a Desktop file if a title was given; otherwise,
46:  # save it to the clipboard
47:  if args.title:
48:    sdate = datetime.now().strftime("%Y%m%d")
49:    desktop = os.environ['HOME'] + "/Desktop/"
50:    fname = f'{desktop}{sdate}-{args.title}.{type}'
51:    shutil.copyfile(args.ssfile, fname)
52:    bname = os.path.basename(fname)
53:  
54:    # Upload the file and print the URL if asked
55:    if args.upload:
56:      year = datetime.now().strftime("%Y")
57:      server = f'user@server.com:path/to/images{year}/'
58:      port = '123456789'
59:      subprocess.run(['scp', '-P', port, fname, server])
60:      bname = urllib.parse.quote(bname)
61:      print(f'https://leancrew.com/all-this/images{year}/{bname}')
62:  else:
63:    subprocess.run(['osascript', '-e', ascript])
64:  
65:  # Delete the temporary file
66:  os.remove(args.ssfile)

The changes are:

The AppleScript that does the work is just one line:

set the clipboard to (read POSIX file "<name of file>" as picture)

Very simple, and I suspect it will work for quite a while. I feel certain that Apple would like to kill AppleScript, but I don’t think it can—too many valuable customers (a group that doesn’t include me) would get very angry at having their longstanding automations die.

One thing that kind of surprised me was the as picture part of the command. Learn AppleScript says that you typically have to include an image type in the command: as GIF picture or as JPEG picture or as TIFF picture. The book suggests that a bare as picture is supposed to work only on PICT images. Never heard of a PICT image? That’s probably because it was the native Mac image format back in the classic days. It was one of Bill Atkinson’s contributions to the Macintosh Toolbox. Nobody saves image files as PICTs anymore.

And yet the unadorned as picture works. I created this test image in Acorn,

Test image

and saved it in several formats: BMP, GIF, HEIC, JPEG, JPEG 2000, PNG, and TIFF. I put the files in a folder named clipboard-test on my Desktop,

Folder of images

and ran this AppleScript for each of the different file extensions:

applescript:
set filePath to path to desktop folder as text
set imageFile to (filePath & "clipboard-test:Test image.bmp") as alias
set the clipboard to (read file imageFile as picture)

Each time I ran the script, the clipboard was loaded with the image, no matter which file was being read. I didn’t have to specify the format.

I suspect that if I were using AppleScript to write image data to a file, it wouldn’t be as forgiving. But process-screenshot uses Python and Pillow to save image files, so I don’t need to worry about AppleScript’s idiosyncrasies for that aspect of the script.


XKCD and Fourier

If you’ve done Fourier series approximations of functions, you had three reactions to today’s XKCD comic:

https://imgs.xkcd.com/comics/gravitational_square_wave.png

First, of course, was a smile of recognition. Then came a “wait a minute” as you realized that the Gibbs phenomenon doesn’t get smaller as you add more terms to the Fourier expansion; it just concentrates itself at the corners:

Gibbs phenomenon examples for square wave

That ringing never goes away. Here, for example, is the plot for

∑k=1,3,5,…994kπsin⁡(kπx)

Square wave approximation

Finally, you realized that thinking like this ruins the joke.


Squaring the triangle with help

Yesterday, Numberphile released a delightful video with James Grime (redundant, I know) in which he walks us through a geometry problem first posed by Henry Dudeney in 1902. It’s known as the “Haberdasher’s Puzzle” and is reproduced in Dudeney’s 1907 book The Canterbury Puzzles. The problem was solved by C.W. McElroy after Dudeney published it in a periodical called the Weekly Dispatch.

The problem is to cut up an equilateral triangle into pieces that can be translated and rotated (not flipped) to fit together into a square. The minimum number of pieces that allow you to do this is four, and here’s the arrangement shown in the video:

Equilateral triangle pieces in video

What James doesn’t do in the video is explain how you position the cuts. Dudeney shows a straightedge-and-compass construction in his book, but I wanted an algebraic solution. This turned out to be more complicated than I anticipated, but I had Mathematica to keep track of the messy algebra.

Let’s start with the equilateral triangle and its component pieces. The points that define the vertices of the triangle and cut lines are given the labels A through J (to avoid confusion, I is omitted).

Equilateral triangle with cuts

The lines GJ and DH are perpendicular to Line EF.

To make the explanation simpler, let’s jump to the end and show how to make a square out of these components. The fun way to do it is in a series of rotations:

  1. Rotate the green piece 180° about Point G.
  2. Rotate the green and orange pieces together 180° about Point E.
  3. Rotate the green, orange, and blue pieces together 180° about Point D.

Here’s an illustration of the steps:

Rotations from triangle to square

In this illustration, the red piece stays put through all the steps.

We can rotate the square to make its sides horizontal and vertical and label the various points to show where they ended up.

Aligned square

The vertices of the equilateral triangle have coalesced into a single point in the interior of the square. The right angles at H and J have split up to form the corners of the square.

Before we get into the algebra of the situation, let’s think about the various dimensions. The side lengths of the square, s, must be equal, so

s=2GJ——=2DH——=EJ——+EH——=FH——+FJ——

A bit of algebra tells us that

2s=EJ——+EH——+FH——+FJ——=(EJ——+FJ——)+(FH——+EH——)

Looking back at the drawing of the equilateral triangle, we see that the terms in the parentheses make up Line EF. Therefore, the length of EF is the side length of the square, s.

Now it’s time for some analytic geometry. We’ll put the origin of our xy coordinate system at Point A, and we’ll assume each side of the equilateral triangle has unit length. The coordinates are:

A:(0,0)B:(1,0)C:(12,32)

The area of the triangle is

12(1)(32)=34

This is the same as the area of the square, so

s=31/42

This will be the length of EF——.

Points D and E are the midpoints of AC and BC:

D:(14,34)E:(34,34)

Since we know the length of Line EF and the position of Point E, we can calculate the position of Point F along Line AB:

F:(14[3−43−3],0)

As I said, it’s kind of messy, but that’s what Mathematica is for. To six decimal places, the x coordinate is 0.254508. This is almost directly under Point D, but not quite.

Now we need the position of Point G. As it happens, it’s exactly 12 unit to the right of Point F:

G:(14[5−43−3],0)

To six decimal places, this is 0.754508.

How do we know this? Consider this drawing:

Second green triangle

The light green triangle, DEH, is congruent to the dark green triangle, GFJ. We know this because:

  1. The right angle at H matches the right angle at J.
  2. The length of DH is equal to the length of GJ (see the discussion above).
  3. The angle at D matches the angle at G because DE is parallel to FG and DH is parallel to GJ.

We therefore have an angle-side-angle congruence. The light green triangle is a duplicate of the dark green triangle, rotated 180° and moved up so its horizontal side is aligned with DE. The distance between D and E is 12, so that must also be the distance between F and G.

With the locations of F and G known, we can find H and J by running lines perpendicular to EF that intersect F and G. The full equations for the positions of H and J are even messier than those of F and G, so I’ll just give the six-place decimal approximations:

H:(0.466506,0.185267)J:(0.538001,0.247746)

If you want to go through the Mathematica that generated these results and the graphics, here it is:


HomeView

Given that I typically take a wait-and-see attitude when Apple introduces a new product, I was surprised to find myself ordering a HomeView within a day of its announcement. I was even more surprised when lots of otherwise pro-Apple people—this guy, for example—were skeptical about its value.

HomeView

Why is this? Well, one reason could be that I don’t already have a HomePod in my kitchen, which makes me behind the times. I’ve been thinking I should get one (or two), but Siri’s deficiencies have kept me back. Also, I like the idea of a status board but didn’t want to dedicate an iPad to that. The recently improved Siri and the HomeView’s touchscreen kill those two birds with one $300 stone.

And I think I have a good place for it in my kitchen. Not on the center island. The two likely spots for it on the island are corners that are already taken:

Another obvious place would be the kitchen desk. Many years ago, we had a laptop computer on the desk, so it has a history of similar use. But the desk isn’t in a great spot for the HomeView—a little too far away from the island to look at it while I’m preparing a meal, and maybe a little too far away from the kitchen table to use as a status board.

But there is a perfect spot at the end of one of the counters. It’s near both the kitchen table and the island. I’ll need to rotate it, depending on where I am, but the annoying family in the video showed that that was no problem. And there’s an outlet right there, so the power cord can be stored neatly.

The counter isn’t a good spot for a FaceTime call, but it’ll be easy to move the HomeView over to the kitchen table when I use it for that.

So by being behind the times for several years, I’m now in the perfect position to be on the cutting edge. With a product that’s been sitting on the shelf for a year or so.