Mimsy Were the Borogoves

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

Place a QR code over an image in macOS

Jerry Stratton, March 23, 2022

QR codes on the macOS command line

You can caption an image with more than just text. I was looking at a QR code the other day and wondered, how hard would it be to create one of my own for my postcard project? So I created this command-line script (Zip file, 4.0 KB) to take an image and some text, and create a QR image from it. Having done that, it was a relatively simple step to layer the QR code over an existing image from my Mac.

I usually just drag a photo from the Photos app onto the command line to generate a captioned image or a QR-coded image.

imagefilepath to image to overlay QR code on
texttext to encode into QR square
--align <alignments>align QR code to top, bottom, right, left, center, right-center, or left-center
--aztecuse Aztec code generator
--bgcolor <color>QR code background color (formats: r,g,b,[f]; FFFFFF[FF]; grey)
--fgcolor <color>QR code foreground color (formats: r,g,b,[f]; FFFFFF[FF]; grey)
--helpprint this help and exit
--level <level>set QR code correction level low, medium, quartile, or high
--opacity <0-100>set the opacity on the background image
--ratio <10-90>ratio of QR code to image size; current: 25.0
--savefilename to save as
--width <size>create an image this many pixels wide

QR codes can be created on their own with just the text that should be encoded. I created the bare QR code for this blog post using:

  • qr "“Be cautious, but be adventurous and the rewards will be tremendous.”—James S. Coan, Basic FORTRAN, p. 83" --save caution.png

The image can be saved as anything; be aware that if you don’t specify a file to save as, it will save it as “QR.png”, and it will erase any existing “QR.png”.

I created the image for this blog post using:

  • qr https://hoboes.com/qr keyboard.jpg --save keyboardQR.jpg --align left

The code can be aligned vertically and horizontally. Most of the alignments are self-explanatory; left-center and right-center align horizontally to the center of the left half of the image or to the right half of the image, respectively.

Alignments make no difference if you’re creating a QR code on its own, without a background image.

QR codes default to black and white, because those really are the best colors. However, QR codes can be any color. The foreground color should be significantly darker than the background color. I’ve added a check for contrast ratio, using the relative luminance calculations from W3C. It will warn if it detects a low contrast ratio, one less than 3.0. I expect this is not an exact science. Further, if you use any transparency in your colors there’s really no way of calculating the contrast ratio because it will vary across the code depending on what’s behind it.

I tend to leave the level to the default (medium), but you can specify low, quartile, or high, which correspond to the L, Q, and H options to QRCodeGenerator.

The default is to use either 400 pixels or the dimensions of the background image for the size of the created image. You can specify any width, however, and it will use that. The height of a bare QR code is the same as its width; the height of a combined QR code and background image will be relative to the source image’s width and the requested width.

URLs should start with http:// or https:// and that text should be lower case. The rest of it should be whatever you need it to be (for the domain) and the web server needs it to be (for the path). For example, https://ClubPadgett.com/, or https://www.hoboes.com/Mimsy/. Capitalization doesn’t matter in the domain, so I capitalized the two words in clubpadgett.com. This reduces the chance of misreading words, as in the infamous Pen Island.

Capitalization does often matter for the path; while many servers will correct incorrect capitalization of the files and folders in the URL, some will not, and why increase the chance of future errors when optional behavior changes?

James S. Coan QR Code Koan

A Coan Koan.

I strongly recommend that you copy and paste your URLs from the browser for use as a QR code. It’s very annoying to have to reprint a flyer after you’ve paid for a thousand copies just because you mistyped the URL for the QR code and it can’t be fixed server-side.

Often it can be fixed server-side, so ask your webmaster. But, again, why increase the chance for error? Copy and paste your URLs while viewing the target page. Just do it! The only exception is if you’re using an URL shortener, and then you’ll copy and paste your URL from the URL shortener manager. You might think you’re not going to mistype a twenty-character URL. Over time, you will always be wrong.

The macOS has QR generation built in. It requires creating a CIFilter using CIQRCodeGenerator.

[toggle code]

  • //generate QR Code
  • guard var qrData = qrText.data(using: String.Encoding.utf8) else {
    • print("Unable to get text as data.")
    • exit(0)
  • }
  • var qrImage:CIImage? = nil
  • if let qrFilter = CIFilter(name: generator) {
    • qrFilter.setValue(qrData, forKey: "inputMessage")
    • if correctionLevel != "" {
      • qrFilter.setValue(correctionLevel, forKey: "inputCorrectionLevel")
    • }
    • guard let qrRawImage = qrFilter.outputImage else {
      • help(message:"Unable to create QR code from " + qrText)
      • exit(0)
    • }
    • let qrScale = qrSide/qrRawImage.extent.size.width
    • qrImage = qrFilter.outputImage?.transformed(by: CGAffineTransform(scaleX: qrScale, y: qrScale))
    • let qrColors = ["inputColor0":fgColor, "inputColor1":bgColor]
    • qrImage = qrImage?.applyingFilter("CIFalseColor", parameters: qrColors)
  • } else {
    • print("Unable to get QR code generator")
    • exit(0)
  • }

The default generator is “CIQRCodeGenerator”. This is the most widely supported QR code. The macOS libraries have the ability to generate Aztec codes using CIAztecCodeGenerator. However, iOS doesn’t appear to support reading Aztec codes yet. If you want to play around with Aztec codes, use --aztec. They’re interesting; the orientation mark is in the center, instead of on the sides. They also don’t appear to support UTF8 characters.

There are three steps to generating a QR code:

  1. Set up the CIFilter with the requested correction level.
  2. Scale the resulting image as necessary.
  3. Adjust the foreground and background colors.

When there’s a background image, I scale the code square to be as large as the lower of the background image’s width and height, because that seems to help readability over scaling the code square to be its final size.

The only tricky bit is that both photos and saving images (as I understand how to do it) requires NSImage. The QR generation code is in Core Image. This requires a simple conversion.

[toggle code]

  • //convert image to desired format
  • var outputImage:NSImage? = nil
  • let nsVersion = NSCIImageRep(ciImage: qrImage!)
  • let qrNSImage = NSImage(size:nsVersion.size)
  • qrNSImage.addRepresentation(nsVersion)
  • if background == nil {
    • outputImage = qrNSImage
  • } else {
    • //code to combine background image and qr image
  • }

If there is no background image, the converted qrImage is directly copied to the outputImage. Saving is handled exactly as it was in the caption script.

If there is a background image, the two must be combined.

[toggle code]

  • //size and potentially resize the image
  • var imageSize = background!.size
  • var qrSize = qrNSImage.size
  • if imageWidth != 0 {
    • let imageRatio = imageWidth/imageSize.width
    • imageSize.width *= imageRatio
    • imageSize.height *= imageRatio
    • qrSize.width *= imageRatio
    • qrSize.height *= imageRatio
  • }
  • let padding = imageSize.width*0.005
  • var qrY:CGFloat
  • var qrX:CGFloat
  • let qrHeight = qrSize.height*qrRatio/100
  • let qrWidth = qrSize.width*qrRatio/100
  • switch vertical {
    • case "top":
      • qrY = imageSize.height-padding-qrHeight
    • case "center":
      • qrY = (imageSize.height-qrHeight)/2
    • default:
      • qrY = padding
  • }
  • switch horizontal {
    • case "left":
      • qrX = padding
    • case "center":
      • qrX = (imageSize.width-qrWidth)/2
    • case "left-center":
      • qrX = (imageSize.width/2-qrWidth)/2
    • case "right-center":
      • qrX = (imageSize.width/2-qrWidth)/2 + imageSize.width/2
    • default:
      • qrX = imageSize.width-qrWidth-padding
  • }
  • let qrLocation = NSRect(x:qrX, y:qrY, width:qrWidth, height:qrHeight)
  • //combine the two images
  • let combinedImage = NSImage(size:imageSize, flipped:false) { (outputRect) -> Bool in
    • background!.draw(in:outputRect)
    • if opacity > 0 {
      • let opacityColor = NSColor(red:1, green:1, blue:2, alpha:opacity/100)
      • opacityColor.setFill()
      • outputRect.fill()
    • }
    • qrNSImage.draw(in:qrLocation)
    • return true
  • }
  • outputImage = combinedImage

The actual combination of the two images is only a few lines. The bulk of this code is for positioning the QR image, and the second-largest bulk is for resizing the image if a specific width has been requested.

I strongly prefer simple options when I write command line scripts; that’s why the options for alignment are simply left, right, center, and so on, instead of specifying a specific numeric location. The same is true for padding. I prefer to find the best padding and use it. Unless I end up needing a different padding, I’m not going to bother adding code to specify different paddings.

Obviously, you could do differently, though I would recommend making padding (for example) be a percentage of the width of the image rather than a specific number of pixels. This ensures that if you change the width, the padding remains relatively the same. The same is true of positioning: if you do need hard positioning, make it a percentage of the image rather than by pixel location.

As I was writing this post, I decided I did need to be able to specify a ratio of QR code size to overall image size, instead of hardcoding 25.0%. That was for the Cherry Almond Ice Cream postcard, that I created with:

  • cat Cherry\ Cream.txt | qr Cherry\ Cream.png --align top --save cherry.png --ratio 23 --fgcolor BE5744 --width 1000

Otherwise, the QR code would have been just barely too close to the recipe’s text.

This is also an example of the upper limit of how much text can be crammed into a small code square. Instead of a URL, I used this QR block to encode the recipe itself, so that people can copy and paste the recipe from a paper postcard. If you attempt to decode the square on your computer screen, you should see the recipe for the Cherry Almond Ice Cream. If you print the image out as a 6x4 postcard, whether you can decode the recipe will depend on the quality of your printer and your camera.

Besides being an example of how to create QR codes on the macOS command line, this script (Zip file, 4.0 KB) is also a good example of how to overlay one image on top of another. You could gut the script of the QR generator and rewrite it to create a collage of multiple images, or provide an inset of one image on top of another, for example.

It’s very cool what you can do with images on the macOS command line, using Swift and the built-in image routines.

In response to Caption this! Add captions to image files: Need a quick caption for an image? This command-line script uses Swift to tack a caption above, below, or right on top of, any image macOS understands.