Generate QR code (offline)

I know that it is possible to create a QR code using AppleScript and a curl command to download, then open a QR code generated with chart.apis.google.com for example.

This required an active internet connection… but the following equivalent written in OBJ-C is able to carry out the same task using essentially the same UI components found in Xcode’s AppleScript ObjC interface designer.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    NSString *info = @"http://codeafterhours.wordpress.com";
    
    // Generation of QR code image
    NSData *qrCodeData = [info dataUsingEncoding:NSISOLatin1StringEncoding]; // recommended encoding
    CIFilter *qrCodeFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
    [qrCodeFilter setValue:qrCodeData forKey:@"inputMessage"];
    [qrCodeFilter setValue:@"M" forKey:@"inputCorrectionLevel"]; //default of L,M,Q & H modes
    
    CIImage *qrCodeImage = qrCodeFilter.outputImage;

    CGRect imageSize = CGRectIntegral(qrCodeImage.extent); // generated image size
    CGSize outputSize = CGSizeMake(240.0, 240.0); // required image size
    CIImage *imageByTransform = [qrCodeImage imageByApplyingTransform:CGAffineTransformMakeScale(outputSize.width/CGRectGetWidth(imageSize), outputSize.height/CGRectGetHeight(imageSize))];

    UIImage *qrCodeImageByTransform = [UIImage imageWithCIImage:imageByTransform];
    self.imgViewQRCode.image = qrCodeImageByTransform;
    
    // Generation of bar code image
    CIFilter *barCodeFilter = [CIFilter filterWithName:@"CICode128BarcodeGenerator"];
    NSData *barCodeData = [info dataUsingEncoding:NSASCIIStringEncoding]; // recommended encoding
    [barCodeFilter setValue:barCodeData forKey:@"inputMessage"];
    [barCodeFilter setValue:[NSNumber numberWithFloat:7.0] forKey:@"inputQuietSpace"]; //default whitespace on sides of barcode
    
    CIImage *barCodeImage = barCodeFilter.outputImage;
    self.imgViewBarCode.image = [UIImage imageWithCIImage:barCodeImage];
}

Source: https://codeafterhours.wordpress.com/2016/03/07/tutorial-how-to-generate-qr-codes-in-ios/

I would like to be able to carry out the same function just with the AppleScriptObjC framework instead of the purely ObjC above.
The key emphasis is offline and without additional terminal tools that need to be installed first (i.e. link text gets converted to QR code and displayed in the NSImageView or similar).
As Google was not very helpful in pointing me in the right direction, I have my fingers crossed that the translation between the code above and its AppleScriptObjC equivalent is straightforward and something I can get a hand with (especially since ObjC is in the name of the target framework).

Thank you in advance.

Here’s some code I’ve used. However, the scaling involves using CGAffineTransformMakeScale(), which I believe is not working from ASObjC in 10.13. You can use an alternative method of scaling instead.

use framework "Foundation"
use framework "AppKit"
use framework "CoreImage"
use scripting additions

set destPath to (POSIX path of (path to desktop folder)) & "Test QR Code.jpg"

my makeQRForString:"https://www.macosxautomation.com/applescript/apps/" ofWidth:360 savingTo:destPath

on makeQRForString:theString ofWidth:theWidth savingTo:posixPath
	set anImageFilter to current application's CIFilter's filterWithName:"CIQRCodeGenerator"
	anImageFilter's setDefaults()
	set thisURLString to current application's NSString's stringWithString:theString
	set thisData to thisURLString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	anImageFilter's setValue:thisData forKey:"inputMessage"
	anImageFilter's setValue:"L" forKey:"inputCorrectionLevel"
	
	set baseImage to anImageFilter's outputImage()
	set baseImageWidth to baseImage's extent()'s |size|()'s width()
	set theScale to theWidth / baseImageWidth
	
	set thisTransform to current application's CGAffineTransform's CGAffineTransformMakeScale(theScale, theScale)
	set thisOutputImage to baseImage's imageByApplyingTransform:thisTransform
	set tiffData to thisOutputImage's TIFFRepresentation()
	tiffData's writeToFile:posixPath atomically:true
end makeQRForString:ofWidth:savingTo:

I have just found out that the CGAffineTransformMakeScale does indeed throw an error, so I found a rather basic method of scaling (which I call at the bottom of the makeQRForString function) that resizes but greatly blurs the output - that is where CGAffineTransformMakeScale would come in handy due to it’s scaling operation before saving, not after.

on scaleImage(myPath)
	tell application "Image Events"
		set thisImage to open myPath
		scale thisImage to size 300
		save thisImage in myPath
		close thisImage
	end tell
end scaleImage

Could you give an example of an alternative method?

Whenever I try to set the NSImageView on the interface builder (imgView) using the following syntax, the error message NSImageCell’s object value must be an NSImage. (error -10000) is thrown. Note: I am pulling from the tiffData variable, not the path to the saved QR code as I have omitted the saving part for now.

imgView's setImage_(tiffData)

This version uses bitmap image reps to do the job. Doing it this way also results in a greyscale file, rather than RGB:

use framework "Foundation"
use framework "AppKit"
use framework "CoreImage"
use scripting additions

set destPath to (POSIX path of (path to desktop folder)) & "Test QR Code.jpg"

my makeQRForString:"https://www.macosxautomation.com/applescript/apps/" ofWidth:360 savingTo:destPath

on makeQRForString:theString ofWidth:theWidth savingTo:posixPath
	set anImageFilter to current application's CIFilter's filterWithName:"CIQRCodeGenerator"
	anImageFilter's setDefaults()
	set thisURLString to current application's NSString's stringWithString:theString
	set thisData to thisURLString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	anImageFilter's setValue:thisData forKey:"inputMessage"
	anImageFilter's setValue:"L" forKey:"inputCorrectionLevel"
	set baseImage to anImageFilter's outputImage()
	
	-- make image rep
	set imageRep to current application's NSBitmapImageRep's alloc()'s initWithCIImage:baseImage
	set theSize to imageRep's |size|()
	set actualWidth to current application's NSWidth(theSize)
	set actualHeight to current application's NSHeight(theSize)
	set theScale to theWidth / actualWidth
	
	-- make greyscale image rep
	set newRep to current application's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:theWidth pixelsHigh:actualHeight * theScale bitsPerSample:8 samplesPerPixel:1 hasAlpha:false isPlanar:false colorSpaceName:(current application's NSCalibratedWhiteColorSpace) bytesPerRow:0 bitsPerPixel:0
	
	-- store graphics state and set new values
	current application's NSGraphicsContext's saveGraphicsState()
	set theContext to current application's NSGraphicsContext's graphicsContextWithBitmapImageRep:newRep
	current application's NSGraphicsContext's setCurrentContext:theContext
	theContext's setShouldAntialias:false
	theContext's setImageInterpolation:(current application's NSImageInterpolationNone)
	
	-- draw from original to new rep
	imageRep's drawInRect:(current application's NSMakeRect(0, 0, theWidth, actualHeight * theScale)) fromRect:(current application's NSZeroRect) operation:(current application's NSCompositeCopy) fraction:(1.0) respectFlipped:false hints:(missing value)
	
	-- restore state and save from new rep
	current application's NSGraphicsContext's restoreGraphicsState()
	set theProps to current application's NSDictionary's dictionaryWithObject:1.0 forKey:(current application's NSImageCompressionFactor)
	set imageData to (newRep's representationUsingType:(current application's NSTIFFFileType) |properties|:theProps)
	(imageData's writeToFile:posixPath atomically:true)
end makeQRForString:ofWidth:savingTo:

You are so right! This is the way to go as the result is sharp and of very high quality.
The final part is drawing this image to into the image view instead of saving it to the file, I have just tried:

imgView's setImage:(imageData)

Which returns: NSImageCell’s object value must be an NSImage. (error -10000) but isn’t it already an NSImage? Is there a special way of translating / drawing this data into the NSImageView instead of the file? Cheers.

You’ll need to do something like:

set theImage to current application's NSImage's alloc()'s initWithData:imageData
imgView's setImage:theImage

Hi, Shane. The code in post #2 seems to have stopped working for me under Mojave. I would appreciate it if you could shed light on the returned error: [format]“{{0.0, 0.0}, {31.0, 31.0}} doesn’t understand the “size” message.” number -1708 from {{0.0, 0.0}, {31.0, 31.0}} to «class size»[/format]

Marc,

I’ve edited the script. The issue is that NSRects used to be returned as records like {origin:{x:0, y:0}, size:{width:100, height:200}} but are now returned as lists of lists: {{0, 0}, {100, 200}}. The change I made will work in either case.

The change was made in High Sierra, and you can read more here:

https://latenightsw.com/high-sierra-applescriptobjc-bugs/

Shane Stanley edited post #4. I will help with correcting the post #2. Thanks for nice script. On Catalina it works:


use framework "Foundation"
use framework "AppKit"
use framework "CoreImage"
use scripting additions

set destPath to (POSIX path of (path to desktop folder)) & "Test QR Code.jpg"

my makeQRForString:"https://www.macosxautomation.com/applescript/apps/" ofWidth:360 savingTo:destPath

on makeQRForString:theString ofWidth:theWidth savingTo:posixPath
	set anImageFilter to current application's CIFilter's filterWithName:"CIQRCodeGenerator"
	anImageFilter's setDefaults()
	set thisURLString to current application's NSString's stringWithString:theString
	set thisData to thisURLString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	anImageFilter's setValue:thisData forKey:"inputMessage"
	anImageFilter's setValue:"L" forKey:"inputCorrectionLevel"
	
	set baseImage to anImageFilter's outputImage()
	set imageRep to current application's NSBitmapImageRep's alloc()'s initWithCIImage:baseImage
	set baseImageWidth to (imageRep's |size|())'s width()
	set theScale to theWidth / baseImageWidth
	
	set thisTransform to current application's CGAffineTransform's CGAffineTransformMakeScale(theScale, theScale)
	set thisOutputImage to baseImage's imageByApplyingTransform:thisTransform
	set tiffData to thisOutputImage's TIFFRepresentation()
	tiffData's writeToFile:posixPath atomically:true
end makeQRForString:ofWidth:savingTo:

For those of you wondering, the following script does the opposite:


-- Created 2018 by Takaaki Naganoya, Piyomaru Software
-- Adapted 25 Aug 2019 by me
use AppleScript version "2.4"
use framework "Foundation"
use framework "QuartzCore"
use scripting additions

property CIDetectorAccuracyHigh : a reference to CIDetectorAccuracyHigh of current application
property NSDictionary : a reference to NSDictionary of current application
property CIDetector : a reference to CIDetector of current application
property |NSURL| : a reference to |NSURL| of current application
property CIImage : a reference to CIImage of current application
property CIDetectorAccuracy : a reference to CIDetectorAccuracy of current application
property CIDetectorTypeQRCode : a reference to CIDetectorTypeQRCode of current application

-- Select image, Convert alias to URL, Generate CIImage
set imageFile to choose file of type {"public.image"}
set imageURL to |NSURL|'s fileURLWithPath:(POSIX path of imageFile)
set imageRef to CIImage's alloc()'s initWithContentsOfURL:imageURL

-- Create detector options with NSDictonary
set optionsDictionary to NSDictionary's dictionaryWithObject:(CIDetectorAccuracyHigh) forKey:(CIDetectorAccuracy)
set theDetector to CIDetector's detectorOfType:(CIDetectorTypeQRCode) context:(missing value) options:optionsDictionary

-- Perform QR code detection
set faceArray to theDetector's featuresInImage:imageRef options:optionsDictionary

-- Get the embeddedURLs of the detected QR code
set embeddedURLs to {}
repeat with theFace in faceArray
	set the end of embeddedURLs to (theFace's messageString()) as string
end repeat
embeddedURLs