Resizing an image

I’m working to gain a basic understanding of NSImage, and I"m currently working with the script included below. It resizes the source image but in every case the newly-created image has a width of 2000 pixels, which is twice the desired result. Why is this?

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

set sourceFile to POSIX path of (choose file)
set targetFile to POSIX path of ((path to desktop as text) & "Reduced Image.png")

resizeImage(sourceFile, targetFile, 1000, 750)

on resizeImage(sourceFile, targetFile, newWidth, newHeight)
	set sourceImage to current application's NSImage's alloc()'s initWithContentsOfFile:sourceFile
	set scaledImage to current application's NSImage's alloc()'s initWithSize:(current application's NSMakeSize(newWidth, newHeight))
	scaledImage's lockFocus()
	sourceImage's setSize:(current application's NSMakeSize(newWidth, newHeight))
	(current application's NSGraphicsContext's currentContext())'s setImageInterpolation:(current application's NSImageInterpolationHigh)
	sourceImage's drawInRect:(current application's NSMakeRect(0.0, 0.0, newWidth, newHeight))
	scaledImage's unlockFocus()
	set theData to current application's NSBitmapImageRep's imageRepWithData:(scaledImage's TIFFRepresentation)
	set theRep to theData's representationUsingType:(current application's NSPNGFileType) |properties|:(missing value)
	theRep's writeToFile:targetFile atomically:true
end resizeImage

The above script is a simple rewrite of the script in the following thread, and I do not really understand how it works. Perhaps I made an edit that is causing this isue.

https://macscripter.net/viewtopic.php?pid=187419

The size property of an image is the size in points the image should appear at. In the case of bitmaps, pre-retina screens this was generally the same as the number of pixels when the image was a bitmap, but now it is more often half that value.

For bitmaps, you need to get the pixelsHigh and/or pixelsWide properties of the bitmaps, and use those values.

If you want to keep the image proportionally scaled you just want to provide a new width.

here’s my Objective C code that I use to scale a new image to a provided width in pixels.
NOTE this is a Extension on NSImage so references to SELF would be your original image.

- (NSImage*)scaledImageForPix:(NSUInteger)aSize {
    NSSize imageSize = [self size];
    NSImage *squareImage = [[NSImage alloc] initWithSize:NSMakeSize(imageSize.width, imageSize.width)];
    NSImage *scaledImage = nil;
    NSRect drawRect;
    
    NSSize scaledSize = NSMakeSize(aSize, aSize);
    
        // make the image square
    if ( imageSize.height > imageSize.width )
    {
        drawRect = NSMakeRect(0, imageSize.height - imageSize.width, imageSize.width, imageSize.width);
    }
    else
    {
        drawRect = NSMakeRect(0, 0, imageSize.height, imageSize.height);
    }
    
        // use native square size if passed zero size
    if ( NSEqualSizes(scaledSize, NSZeroSize) )
    {
        scaledSize = drawRect.size;
    }
    
    scaledImage = [[NSImage alloc] initWithSize:scaledSize];
    
    [squareImage lockFocus];
    [self drawInRect:NSMakeRect(0, 0, imageSize.width, imageSize.width) fromRect:drawRect operation:NSCompositeSourceOver fraction:1.0];
    [squareImage unlockFocus];
    
        // scale the image to the desired size
    
    [scaledImage lockFocus];
    [squareImage drawInRect:NSMakeRect(0, 0, scaledSize.width, scaledSize.height) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
    [scaledImage unlockFocus];
    
        // convert back to readable bitmap data
    
    CGImageRef cgImage = [scaledImage CGImageForProposedRect:NULL context:nil hints:nil];
    NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
    NSImage *finalImage = [[NSImage alloc] initWithSize:scaledImage.size];
    [finalImage addRepresentation:bitmapRep];
    return finalImage;
}

Thanks Shane and technomorph for your responses. I appreciate your help.

Also maybe the sourceImage size isn’t being changed because you’ve locked focus on the scaledImage.

And here’s some code that will create a new rect with proportion scaling (IE may distort)

(NOTE this is note my code)
Note your should be able to substitute all the CG’s for NS as they are bridged.
For the std::min function you’ll have to do a if / then and compare the widthFactor, heightFactor.
And I would probably create the NSRect before returning it.


- (CGRect) proportionallyScale:(CGSize)fromSize toSize:(CGSize)toSize
{
    CGPoint origin = CGPointZero;
    CGFloat width = fromSize.width, height = fromSize.height;
    CGFloat targetWidth = toSize.width, targetHeight = toSize.height;

    float widthFactor = targetWidth / width;
    float heightFactor = targetHeight / height;

    CGFloat scaleFactor = std::min(widthFactor, heightFactor);

    CGFloat scaledWidth = width * scaleFactor;
    CGFloat scaledHeight = height * scaleFactor;

    if (widthFactor < heightFactor)
        origin.y = (targetHeight - scaledHeight) / 2.0;
    else if (widthFactor > heightFactor)
        origin.x = (targetWidth - scaledWidth) / 2.0;
    return {origin, {scaledWidth, scaledHeight}};
}

The dimensions of my source image are 3264 x 2448, and the dimensions of the image created by my script are 2000 x 1500. So, the image size is being changed.

Thanks for the code suggestion. I’ll try to work that into my script.

Here’s a slightly different approach:

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

set sourceFile to POSIX path of (choose file)
set targetFile to POSIX path of ((path to desktop as text) & "Reduced Image.png")

resizeImage(sourceFile, targetFile, 1000, 750)

on resizeImage(sourceFile, targetFile, newWidth, newHeight)
	set sourceImage to current application's NSImage's alloc()'s initWithContentsOfFile:sourceFile
	set aRep to current application's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:newWidth pixelsHigh:newHeight bitsPerSample:8 samplesPerPixel:4 hasAlpha:true isPlanar:false colorSpaceName:(current application's NSCalibratedRGBColorSpace) bytesPerRow:0 bitsPerPixel:0
	set newSize to {width:newWidth, height:newHeight}
	aRep's setSize:newSize
	
	current application's NSGraphicsContext's saveGraphicsState()
	set theContext to current application's NSGraphicsContext's graphicsContextWithBitmapImageRep:aRep
	current application's NSGraphicsContext's setCurrentContext:theContext
	theContext's setShouldAntialias:false -- change to suit
	theContext's setImageInterpolation:(current application's NSImageInterpolationNone) -- change to suit
	sourceImage's drawInRect:(current application's NSMakeRect(0, 0, newWidth, newHeight)) fromRect:(current application's NSZeroRect) operation:(current application's NSCompositeCopy) fraction:(1.0)
	current application's NSGraphicsContext's restoreGraphicsState()
	
	set theData to aRep's representationUsingType:(current application's NSPNGFileType) |properties|:(missing value)
	theData's writeToFile:targetFile atomically:true
end resizeImage

Shane. I tested your script on a number of images and it worked great. I read through the script and have a general understanding of its operation, and I’ll have a good understanding after spending some time with the documentation. Many thanks.

I looked at a script written by Fredrik71 in the thread linked below. I was curious how this script compared with the script included above and with a script that uses the sips utility. My test image was a photo taken with a Pixel 4a phone, but I ran additional tests with many other images from various sources. The goal was to reduce the width of the photo to 1000 pixels.

The results were:

SCRIPT - FILE SIZE - TIMING RESULT

AppKit - 102KB - 52 milliseconds
CoreImage 113KB - 78 milliseconds
Sips - 160KB - 87 milliseconds

None of the above seems very significant, but I was surprised that the image created with the AppKit script was degraded as compared with the original with a somewhat dark cast and a lack of definition. The other scripts returned photos that were not at all degraded. Perhaps my implementation of the AppKit script is erroneous in some respect. It’s important to note that the AppKit and CoreImage scripts do not include Exif and other significant metadata in the newly-created images.

Most users are best advised to use sips or Image Events or one of the many utilities that will resize images. For learning purposes, I find this topic of interest.

The AppKit script is:

-- Revised 2021.10.18 to fix lack of definition and dark color cast

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

on main()
	set sourceFile to POSIX path of (choose file)
	set targetFile to POSIX path of ((path to desktop as text) & "Reduced Image.jpg")
	set {existingWidth, existingHeight} to getImageSize(sourceFile)
	set newWidth to 1000
	set newHeight to round ((newWidth / existingWidth) * existingHeight)
	
	set sourceImage to current application's NSImage's alloc()'s initWithContentsOfFile:sourceFile
	set aRep to current application's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:newWidth pixelsHigh:newHeight bitsPerSample:8 samplesPerPixel:4 hasAlpha:true isPlanar:false colorSpaceName:(current application's NSDeviceRGBColorSpace) bytesPerRow:0 bitsPerPixel:0
	set newSize to {width:newWidth, height:newHeight}
	aRep's setSize:newSize
	
	current application's NSGraphicsContext's saveGraphicsState()
	set theContext to current application's NSGraphicsContext's graphicsContextWithBitmapImageRep:aRep
	current application's NSGraphicsContext's setCurrentContext:theContext
	theContext's setShouldAntialias:false -- change to suit
	theContext's setImageInterpolation:(current application's NSImageInterpolationDefault) -- change to suit
	sourceImage's drawInRect:(current application's NSMakeRect(0, 0, newWidth, newHeight)) fromRect:(current application's NSZeroRect) operation:(current application's NSCompositeCopy) fraction:(1.0)
	current application's NSGraphicsContext's restoreGraphicsState()
	
	set theRep to aRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:0.5, NSImageProgressive:false}
	theRep's writeToFile:targetFile atomically:true
end main

on getImageSize(theFile)
	set theImage to current application's NSBitmapImageRep's imageRepWithContentsOfFile:theFile
	set pixelWidth to theImage's pixelsWide()
	set pixelHeight to theImage's pixelsHigh()
	return {pixelWidth, pixelHeight}
end getImageSize

main()

The CoreImage script is:

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

on main()
	set sourceFile to "/Users/Robert/Working/Test Photo.jpg"
	set targetFile to POSIX path of ((path to desktop as text) & "Reduced CoreImage.jpg")
	set targetWidth to 1000
	
	set sourceFile to current application's |NSURL|'s fileURLWithPath:sourceFile
	set sourceImage to current application's CIImage's imageWithContentsOfURL:sourceFile options:(missing value)
	
	set {sourceWidth, sourceHeight} to {item 1 of item 2 of sourceImage's extent(), item 2 of item 2 of sourceImage's extent()}
	set imageScale to targetWidth / sourceWidth
	
	set resizeFilter to current application's CIFilter's filterWithName:"CILanczosScaleTransform" withInputParameters:{inputImage:sourceImage, inputScale:imageScale, inputAspectRatio:1.0}
	set outputImage to resizeFilter's outputImage()
	set imageRep to current application's NSBitmapImageRep's alloc()'s initWithCIImage:outputImage
	
	set theRep to imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:0.5, NSImageProgressive:false}
	theRep's writeToFile:targetFile atomically:true
end main

main()

The sips script is:

set sourceFile to "/Users/Robert/Working/Test Photo.jpg"
set targetFile to POSIX path of ((path to desktop as text) & "Reduced Sips.jpg")

do shell script "sips -Z 1000 " & quoted form of sourceFile & " --out " & quoted form of targetFile

Fredrik71’s script can be found at:

https://macscripter.net/viewtopic.php?id=48708

Play with the two lines with the comment “-- change to suit”. The defaults I used are probably more suited to non-photographic content (text, etc).

Thanks Shane. I changed NSImageInterpolationNone to NSImageInterpolationDefault, and that fixed the issue. I don’t know why I didn’t try that before. The antialias setting made no difference.

BTW, the Core Image script often does not retain the photo orientation, and the AppKit script does. CoreImage has some methods that deal with the setting of orientation, and I assume something needs to be done with them.

Fredrik71. Sorry for any confusion with my earlier post, which I deleted.

My test image is a photo taken in landscape mode with my phone. When viewed in Preview, the orientation is shown as “3 (rotated 180 degrees)”.

I’ve modified my script to implement your suggestion, and it works great when applied to my test image. It rotates the image 180 degrees and then resizes it.

Obviously, not every image needs to be rotated 180 degrees. So, it would appear all I need to do is get the orientation of the source image and to then apply the necessary orientation change to the resized image using a conditional as you suggest. I don’t know how to get the orientation of the source image, but I’m sure some research will yield a suitable answer. Other than that, the changes to my script are relatively simple. Thanks for the help.

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

on main()
	set sourceFile to POSIX path of (choose file)
	set targetFile to POSIX path of ((path to desktop as text) & "Reduced CoreImage.jpg")
	set targetWidth to 1000
	
	set sourceFile to current application's |NSURL|'s fileURLWithPath:sourceFile
	set sourceImage to current application's CIImage's imageWithContentsOfURL:sourceFile options:(missing value)
	
	set {sourceWidth, sourceHeight} to {item 1 of item 2 of sourceImage's extent(), item 2 of item 2 of sourceImage's extent()}
	set imageScale to targetWidth / sourceWidth
	
	set inputImage to sourceImage's imageTransformForOrientation:(current application's kCGImagePropertyOrientationDown)
	set sourceImage to sourceImage's imageByApplyingTransform:inputImage
	
	set resizeFilter to current application's CIFilter's filterWithName:"CILanczosScaleTransform" withInputParameters:{inputImage:sourceImage, inputScale:imageScale, inputAspectRatio:1.0}
	set outputImage to resizeFilter's outputImage()
	set imageRep to current application's NSBitmapImageRep's alloc()'s initWithCIImage:outputImage
	
	set theRep to imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:0.5, NSImageProgressive:false}
	theRep's writeToFile:targetFile atomically:true
end main

main()

This is my final Core Image script. It’s written for resizing JPG images but could be edited for use with other image types.

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

set sourceFile to POSIX path of (choose file)
set targetFile to POSIX path of ((path to desktop as text) & "Resized Image.jpg")
set targetWidth to 1000

resizeImage(sourceFile, targetFile, targetWidth)

on resizeImage(sourceFile, targetFile, targetWidth)
	set sourceFile to current application's |NSURL|'s fileURLWithPath:sourceFile
	set sourceImage to current application's CIImage's imageWithContentsOfURL:sourceFile options:(missing value)
	
	set sourceProperties to (sourceImage's valueForKey:"properties") as record
	set sourceWidth to PixelWidth of sourceProperties
	try
		set sourceOrientation to Orientation of sourceProperties
	on error
		set sourceOrientation to 1
	end try
	
	if sourceOrientation = 3 then
		set sourceImage to sourceImage's imageByApplyingOrientation:(current application's kCGImagePropertyOrientationDown)
	else if sourceOrientation = 6 then
		set sourceImage to sourceImage's imageByApplyingOrientation:(current application's kCGImagePropertyOrientationRight)
	else if sourceOrientation = 8 then
		set sourceImage to sourceImage's imageByApplyingOrientation:(current application's kCGImagePropertyOrientationLeft)
	end if
	
	set imageScale to targetWidth / sourceWidth
	set targetImage to current application's CIFilter's filterWithName:"CILanczosScaleTransform" withInputParameters:{inputImage:sourceImage, inputScale:imageScale, inputAspectRatio:1.0}
	set targetImage to targetImage's outputImage()
	
	set imageRep to current application's NSBitmapImageRep's alloc()'s initWithCIImage:targetImage
	set imageRep to imageRep's representationUsingType:(current application's NSJPEGFileType) |properties|:{NSImageCompressionFactor:0.5, NSImageProgressive:false}
	imageRep's writeToFile:targetFile atomically:true
end resizeImage