Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Avoiding lockFocus when drawing images in Swift on macOS

Jerry Stratton, July 29, 2020

Lovecraft Lies Dreaming: “Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn.” over Bryan Moore’s Lovecraft bust at the Providence Athenaeum.; H. P. Lovecraft; ascii art

Lovecraft lies dreaming…

Among the coolest uses of command line scripts in Swift on the Macintosh are those that intermediate between images and text. These are the kind of scripts that bring me back to ma jeuness of staying up into the morning hours programming.

My favorite of these wee hour efforts is a script I wrote for 42 Astoundingly Useful Scripts and Automations for the Macintosh to turn photographs into ascii art. I wrote it to do basically what ascii art used to be, an ingenious method of greyscaling images at very low resolution. But I’ve since extended the script to include color, color overlays, and random color, as well as sequential text instead of text chosen for its density.

I wrote another script more recently that takes standard input and converts it to an image of the text. Originally for converting tabular data to an image I now use it for taking paragraphs and wrapping them and justifying them or aligning them right or center. It is both a more serious program than asciiArt and more frivolous. It has pretty much only one use case, online outlets that accept images but not tables. If you’ve read 42 Astounding Scripts, this is an image-oriented variation of my alignTabs script and makeHTMLTable script.

Both asciiArt and text2image take text and create images out of the text. Almost all of the examples of creating images in Apple’s documentation and on sites such as stack overflow assume that you’re creating images to display to the screen. They use lockFocus() to create the image. Most of the few remaining examples continue to use lockFocus probably because that’s what gets used most often for examples.

That’s a problem, because lockFocus does things that only make sense when displaying to a screen, such as automatically changing the number of dots in the image depending on whether your screen is a retina screen. This means that a command-line script written using lockFocus will change its behavior depending on what kind of a monitor you’re using.

While writing text2image I ran across Apple’s page recommending against lockFocus for creating image files. On that page, it said something, albeit without examples, that I hadn’t seen before:

The code in the block should be the same code that you would use between the lockFocus and unlockFocus methods.

That was the key to jettisoning lockFocus.

ASCII Art

The asciiArt script is the only script in 42 Astounding Scripts that I wrote specifically because I was gathering my most-used scripts for a book. While I had long wanted to be able to create ascii artwork from my photos, I hadn’t gotten around to it. Partly this is because I wanted to do it long before the computers I owned had the capability, and by the time they did it was one of those things on the back back burner.

Now that I have it, I use it all the time, so plug for the book.

I originally used lockFocus for the ASCII art script because that’s what all the examples I found used. Because of that, I had to have special code to look up mainScreen.backingScaleFactor so that I could adjust the size of the image.

[toggle code]

  • //retina screens provide multiple pixels per dot
  • guard let mainScreen = NSScreen.main else {
    • print("Unable to get screen for scale factor.")
    • exit(0)
  • }
  • //calculate new weidth and height
  • let newWidth:CGFloat = CGFloat(columns)/mainScreen.backingScaleFactor

If I didn’t do this, I got too many dots in the image.

By shifting from using lockFocus() to using NSImage(size: flipped:) { code }, I was able to not only remove that code, but shorten the part of the script that created the image from 11 lines to 8 lines, and two of those eight were closing braces.

This is the old and new code for resizing the image to have the same number of pixels as the requested character width:

[toggle code]

  • //resize image to new width and height
  • let newRect = CGRect(x: 0, y: 0, width: newWidth, height: newHeight)
  • let newImage = NSImage(size: NSMakeSize(newWidth, newHeight))
  • newImage.lockFocus()
  • oldImage.draw(in: newRect)
  • newImage.unlockFocus()
  • let newImage = NSImage(size:NSMakeSize(newWidth, newHeight), flipped: false) { (newRect) -> Bool in
    • oldImage.draw(in: newRect)
    • return true
  • }

The point of this snippet is to create a one-to-one correspondence between each pixel and the corresponding character from the ASCII palette.

And here’s the old and new code for preparing the ascii artwork for saving to an image file if requested:

[toggle code]

  • let outputSize = imageLines.size()
  • let outputImage = NSImage(size: outputSize)
  • let outputRect = CGRect(x: 0, y: 0, width: outputSize.width, height: outputSize.height)
  • //draw the text in the image/rect
  • outputImage.lockFocus()
  • imageLines.draw(in: outputRect)
  • outputImage.unlockFocus()
  • let outputImage = NSImage(size:imageLines.size(), flipped: false) { (outputRect) -> Bool in
    • imageLines.draw(in: outputRect)
    • return true
  • }

If you want to see the full script, it’s in 42 Astounding Scripts in the photos and music chapter. I’ve updated both the ebook and the print version with the new code.

Text and table to image converter

One-hit wonders: A collection of one-hit wonders from 1965 to 1997.; one-hit wonders

What is this a list of?

I needed a script to convert tables to images for Smashwords several weeks ago, and since I already had text-to-image figured out for asciiArt it wasn’t difficult. I ended up adding a bunch of other features, too, such as putting a border around the resulting image.

[toggle code]

  • let outputImage = NSImage(size:outputSize, flipped: false) { (outputRect) -> Bool in
    • //fill the image with the background color
    • backgroundColor.setFill()
    • outputRect.fill()
    • //draw the text
    • imageLines.draw(in: textRect)
    • //draw the border if requested
    • if borderPadding > 0 {
      • foregroundColor.setFill()
      • outputRect.frame(withWidth:borderWidth)
    • }
    • return true
  • }

Again, I don’t need to create a special outputRect just for drawing the image; NSImage creates it for me and discards it for me after it’s been used. All I have to do is return true if I’ve successfully created the image. The code between the two outer braces, sans return statement, are exactly the code that would go between lockFocus() and unlockFocus() if drawing for the screen.

This snippet creates the image with a background color, fills in the background color (because text only draws the background up to the end of the text), and then the text. If there’s a border, it makes it the same color as the text.

If you want to see the full script, I describe it with examples at Text to image filter for Smashwords conversions.

Resize NSImage

You can use the same technique to resize an image.

[toggle code]

  • func scaleImage(image:NSImage, width:CGFloat) -> NSImage {
    • //calculate new height from width
    • let height = image.size.height*width/image.size.width
    • let scaledSize = NSSize(width: Int(width), height: Int(height))
    • //resize image
    • let scaledImage = NSImage(size:scaledSize, flipped: false) { (resizedRect) -> Bool in
      • image.draw(in: resizedRect)
      • return true
    • }
    • return scaledImage
  • }

As an example of how really nice the Apple routines sometimes are, I did this without any examples. Or, more specifically, I was looking at some very complicated examples and wondered what would happen if I just took the text-drawing code and replaced it with image.draw(). I shouldn’t have been surprised. The description of draw(in:) is:

This method draws the entire image in the specified rectangle, scaling the image as needed.

If I’d thought of searching on scaling an NSImage instead of resizing one, I might have found it without having to guess. Just trying what ought to work is something that works well when using applications on macOS; it’s a pretty good feeling when it happens while programming.

  1. <- Text to image filter
  2. Battery lamp ->