Mimsy Were the Borogoves

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

ISBN (128) Barcode generator for macOS

Jerry Stratton, May 25, 2022

Southern Living Index back cover

There are currently four code generators within CIFilter. The QR code generator that I wrote about earlier is probably the most useful for general use, although the Aztec code generator has some interesting potential for encoding more text into smaller spaces. There’s also a PDF417 barcode generator, commonly used for id cards and passes.

There is also a Code 128 barcode generator, commonly used for UPC codes on books. If you publish a book on Amazon, for example, Amazon gives you the option of putting your own barcode on the back of the book. Otherwise, their barcode will go on without regard for your back cover image.

I recently wrote a script to create an ISBN barcode (Zip file, 3.0 KB) because I wanted to place my own barcode on my Unofficial Index to the Southern Living Cookbook Library. The code for creating a Code 128 barcode in Swift is pretty much exactly the same as for creating a QR code in Swift.

[toggle code]

  • //generate barcode
  • let isbnData = isbn.data(using: String.Encoding.utf8)
  • var isbnImage:CIImage? = nil
  • if let barcodeFilter = CIFilter(name: "CICode128BarcodeGenerator") {
    • barcodeFilter.setValue(isbnData, forKey: "inputMessage")
    • barcodeFilter.setValue(0.0, forKey: "inputQuietSpace")
    • isbnImage = barcodeFilter.outputImage?.transformed(by:CGAffineTransform(scaleX: scale, y: 1.6*scale))
  • } else {
    • print("Unable to get barcode generator")
    • exit(0)
  • }

Just as with the QR code, the string to be encoded (in this case, in the variable isbn) must be converted to raw NSData.1

I set the quiet space around the barcode to zero, because I’m going to handle the whitespace later using padding. The default barcode is very thin, and will have to be very wide to meet Amazon’s height requirements, so I scale the height of the barcode up by 60% more than its width.

My Swift barcode creator replaces a much simpler Python version:

  • #!/usr/local/bin/python3
  • #create an ISBN barcode
  • # Jerry Stratton astoundingscripts.com
  • import barcode
  • import argparse
  • parser = argparse.ArgumentParser(description='Create barcode from 13-digit ISBN.')
  • parser.add_argument('isbn', help='Thirteen-digit ISBN')
  • parser.add_argument('filename', help='filename to save to')
  • args = parser.parse_args()
  • coder = barcode.get_barcode_class('isbn13')
  • isbn = coder(args.isbn)
  • isbn.save(args.filename)

This is obviously a much simpler script, but macOS’s image-creation routines produce much higher quality images than Python’s. The Python version does have one huge advantage over the macOS routines: it creates SVG images, that is, vector images. There’s no easy way to save an NSImage or CIImage as SVG. It is easy to save as PDF, and I have that built into the script. But that doesn’t do a whole lot of good with the page layout software I use, Scribus. On exporting the cover as PDF for upload to Amazon, Scribus will convert all embedded PDFs into bitmaps, rather than maintain them in vector format.

When you run the script on the command line, you type as many ISBNs as you want with the command. The script validates each ISBN in three ways: it verifies that the ISBN contains only digits (or dashes, but they’ll be stripped), that the ISBN is exactly 13 characters, and that the ISBN’s checksum matches. A 13-digit ISBN checksums much more easily than the old 10-digit format.

[toggle code]

  • //validate checksum
  • var checksum = 0
  • for (index, digit) in isbn.enumerated() {
    • let digitValue = digit.wholeNumberValue!
    • checksum += digitValue
    • if index % 2 == 1 { checksum += 2*digitValue }
  • }
  • if checksum % 10 != 0 {
    • help(message:isbn + " does not checksum correctly.")
  • }

The script then creates a more human-readable ISBN by inserting dashes at the appropriate locations:

  • let isbnFormatted = NSMutableString(string:isbn)
  • isbnFormatted.insert("-", at: 12)
  • isbnFormatted.insert("-", at: 6)
  • isbnFormatted.insert("-", at: 4)
  • isbnFormatted.insert("-", at: 3)

The script creates three things: an ISBN formatted with dashes; an ISBN broken into the three parts that get inset into the bottom of the barcode; and the barcode image itself. The two former, the readable ISBNs, are NSAttributedStrings. The barcode is an NSImage. Once these three parts are created, they need to be combined into a single NSImage:

[toggle code]

  • //combine text and barcode
  • let imageSize = NSSize(
    • width: barcodeSize.width+borderWidth*2+paddingX*2,
    • height: barcodeSize.height+captionHeight+labelHeight+borderWidth*2+paddingTop
  • )
  • let bug = NSImage(size:imageSize, flipped:false) { (outputRect) -> Bool in
    • //background color
    • backgroundColor.setFill()
    • outputRect.fill()
    • //barcode
    • let barcodeRect = NSRect(x:paddingX, y:labelHeight, width:barcodeSize.width, height:barcodeSize.height)
    • barcode.draw(in: barcodeRect)
    • //caption
    • let captionY = outputRect.height-borderWidth-paddingTop/2-captionHeight-captionHeight*textAdjustment
    • let captionRect = NSRect(x:0, y:captionY, width:outputRect.width, height:captionHeight)
    • caption.draw(in: captionRect)
    • //label first character
    • let labelY = barcodeRect.minY - labelHeight/2
    • var labelX = barcodeRect.minX - label9.size().width*1.2
    • var labelRect = NSRect(x:labelX, y:labelY, width:label9.size().width, height:label9.size().height)
    • label9.draw(in:labelRect)
    • //label left half
    • labelX = barcodeRect.minX + barcodeRect.width/4 - labelA.size().width/2
    • labelRect = NSRect(x:labelX, y:labelY, width:labelA.size().width, height:labelA.size().height)
    • backgroundColor.setFill()
    • labelRect.fill()
    • labelA.draw(in:labelRect)
    • //label right half
    • labelX = barcodeRect.minX + barcodeRect.width*3/4 - labelB.size().width/2
    • labelRect = NSRect(x:labelX, y:labelY, width:labelB.size().width, height:labelB.size().height)
    • backgroundColor.setFill()
    • labelRect.fill()
    • labelB.draw(in:labelRect)
    • //border
    • borderColor.setFill()
    • outputRect.frame(withWidth:borderWidth)
    • return true
  • }
Southern Living Index ISBN

I generated this using the command line: barcoder 9798430325121.

The file generation has one weird trick in it. From my inadvertent experimentation, Amazon’s secondary validation2 does not care how many pixels the barcode is. It checks the DPI setting embedded in the image (which itself is embedded into the PDF that they require) and ignores the actual density of pixels in the image.

That is, when I uploaded a PDF cover with the UPC embedded as a 1781 x 1025 image, I was emailed this generic cover error:

The barcode in your cover file doesn't meet our manufacturing specifications. Included barcodes should be in vector format or be a minimum of 300 DPI for rasterized images on the back cover. The barcode must be 2" (50.8 mm) wide, 1.2" (30.5 mm) tall, and at least 0.25" (6 mm) from the cover’s edge. The barcode must be pure black on a white background with enough white space around it to allow for successful scans. To meet our manufacturing specifications, you can update the barcode on your cover file and upload a new file. Or, you can remove the barcode from your cover file, upload a new file, and uncheck the box below the cover upload section to indicate your file doesn't have a barcode. If your file doesn't have a barcode, we'll add one for you.

Checking through each of the requirements, the image seemed to be correct. On a whim, I uploaded the very same image, still 1781 x 1025, but with the metadata changed to mark it as 300 dpi. That was accepted.

It could just be coincidence, of course. Sometimes secondary validation at sites like Amazon and SmashWords can be arbitrary. Fortunately, changing the DPI of an NSImage is easy. It is not, however, obvious:

  • //set the resolution to the desired dpi
  • let dpiScale = 72.0/300.0
  • bitmapVersion.size = NSSize(width: bitmapVersion.size.width*dpiScale, height: bitmapVersion.size.height*dpiScale)

I passed over this solution several times, thinking it would change the actual pixel size of the image, while looking for a method call that would get or set the dpi of an NSImage or NSBitmapImageRep. There didn’t appear to be one. Apparently, once an NSImage is transformed into an NSBitmapImageRep, the dpi is changed by changing the NSBitmapImageRep’s size property.

Changing the size property on the bitmap does not change the number of pixels; it changes the number of pixels per “real” measurement. Since the default is 72 dpi3, I set the scale factor to 72.0/300.0 to reduce the “real size” of the image, thus increasing the resolution of the image.

The number of pixels remains exactly the same before and after changing the bitmap’s size property. Only the metadata changes.

Except for that one weird trick, this script generates a PDF or PNG exactly the same way as my QR code generator does. It’s simplified, because rather than require specifying the image filename on the command line, the script uses the formatted ISBN as the filename, appending “.pdf” or “.png” as necessary.

  • barcoder 9798430325121
  • barcoder 9798430325121 --pdf

The first line creates a PNG file at 300 dpi for the ISBN 979-8-43-032512-1. The second line creates a PDF file with the same image.

The only option in this script is --pdf for generating the images as PDFs. All other items on the command line are assumed to be ISBNs and treated accordingly.

barcoder (Zip file, 3.0 KB)

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.

  1. I use utf8 encoding because I know it works—due to the QR script—and becauseI tend to copy code from script to script. Technically, Code 128 won’t need that, because Code 128 barcodes only use alphanumeric characters.

  2. After the initial validation immediately after uploading the file. The initial validation provides immediately-identified errors on the web page after uploading. The secondary validation is an email of problems discovered later. The barcode definitely had more than 300 “dots” per inch. But the metadata still said 72, and changing just the metadata seemed to allow the cover to be accepted without a secondary error.

  3. Or ppi, depending. It appears that the main difference between DPI and PPI is whether the image is destined for the screen or for a printer.

  1. <- QR over image