NSImage become blurry after transformation - cocoa

I rotated a NSImage at its center with the NSAffineTransform class and it works.
The code is here:
- (NSImage*)rotateImage: (NSImage*)myImage angle:(float)rotateAngle {
NSRect imageBounds = {NSZeroPoint, [myImage size]};
NSRect transformedImageBounds = imageBounds;
NSBezierPath* boundsPath = [NSBezierPath bezierPathWithRect:imageBounds];
NSAffineTransform* transform = [NSAffineTransform transform];
// calculate the bounds for the rotated image
if (rotateAngle != 0) {
NSAffineTransform* temptransform = [NSAffineTransform transform];
[temptransform rotateByDegrees:rotateAngle];
[boundsPath transformUsingAffineTransform:temptransform];
NSRect rotatedBounds = {NSZeroPoint, [boundsPath bounds].size};
// center the image within the rotated bounds
imageBounds.origin.x += (NSWidth(rotatedBounds) - NSWidth (imageBounds))/2;
imageBounds.origin.y += (NSHeight(rotatedBounds) - NSHeight (imageBounds))/2;
// set up the rotation transform
[transform translateXBy:+(NSWidth(rotatedBounds) / 2) yBy:+ (NSHeight(rotatedBounds) / 2)];
[transform rotateByDegrees:rotateAngle];
[transform translateXBy:-(NSWidth(rotatedBounds) / 2) yBy:- (NSHeight(rotatedBounds) / 2)];
transformedImageBounds = rotatedBounds;
}
// draw the original image into the new image
NSImage* transformedImage = [[NSImage alloc] initWithSize:transformedImageBounds.size];;
[transformedImage lockFocus];
[transform concat];
[myImage drawInRect:imageBounds fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0] ;
[transformedImage unlockFocus];
return transformedImage;
}
But the quality of the resulted image is very bad.
How to solve it?
Some say the "lockFocus" methods caused this but I hv no idea of how to do this without using the method.

I'm not sure what it's place is in this pipeline, but if you can get hold of the NSGraphicsContext (which might be as easy as:
NSGraphicsContext *myContext =
[NSGraphicsContext currentContext];
after your lockFocus), you can try
[myContext setImageInterpolation:NSImageInterpolationHigh]
to ask Cocoa to use its best algorithms for scaling the image. If you switch to using the CGImageRef API, I know you can get more control over the interpolation setting pretty easily.

Related

NSImage rotation using block code blurry on non-retina

I am using the following function to rotate an NSImage. It uses block code in order to render properly on retina. The problem now is that it renders blurry on non-retina screens.
I tried checking for a retina display [NSWindow backingScaleFactor] and then using a non-block method on non-retina displays, but the is a case in which it doesn't work - if the user has a retina Mac but is displaying to an external non-retina display, the check backingScaleFactor reports a retina screen. I then found this document on Apple's developer site:
APIs for Supporting High Resolution
https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/APIs/APIs.html
But it's frankly inscrutable. How can the following function be modified to not blur on non-retina displays?
- (NSImage *)imageRotatedByDegrees:(CGFloat)degrees {
// calculate the bounds for the rotated image
NSRect imageBounds = {NSZeroPoint, [self size]};
NSBezierPath *boundsPath = [NSBezierPath bezierPathWithRect:imageBounds];
NSAffineTransform *transform = [NSAffineTransform transform];
[transform rotateByDegrees:degrees];
[boundsPath transformUsingAffineTransform:transform];
NSRect rotatedBounds = {NSZeroPoint, [boundsPath bounds].size};
// center the image within the rotated bounds
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth (imageBounds) / 2);
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight (imageBounds) / 2);
NSImage *rotatedImage = [NSImage imageWithSize:rotatedBounds.size flipped:NO drawingHandler:^BOOL (NSRect dstRect) {
// set up the rotation transform
NSAffineTransform *transform=[NSAffineTransform transform];
[transform translateXBy:+(NSWidth(rotatedBounds) / 2) yBy:+(NSHeight(rotatedBounds) / 2)];
[transform rotateByDegrees:degrees];
[transform translateXBy:-(NSWidth(rotatedBounds) / 2) yBy:-(NSHeight(rotatedBounds) / 2)];
// draw the original image, rotated, into the new image
[transform concat];
[self drawInRect:imageBounds fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
return YES;
}];
return rotatedImage;
}
Thanks
Try this
(NSImage*)rotateImagedByDegrees:(CGFloat)degrees Image:(NSImage *)sourceImage
{
NSImage *image = sourceImage;
if (degrees == 0)
{
return image;
}
else
{
NSRect imageBounds = {NSZeroPoint, [sourceImage size]};
NSSize rotatedSize = NSMakeSize(sourceImage.size.height, sourceImage.size.width) ;
NSImage* rotatedImage = [[NSImage alloc] initWithSize:rotatedSize] ;
NSBezierPath* boundsPath = [NSBezierPath bezierPathWithRect:imageBounds];
NSAffineTransform* transform = [NSAffineTransform transform];
[transform rotateByDegrees:-1.0 * degrees];
[boundsPath transformUsingAffineTransform:transform];
NSRect rotatedBounds = {NSZeroPoint, [boundsPath bounds].size};
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2);
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2);
rotatedImage = [NSImage imageWithSize:rotatedBounds.size flipped:NO drawingHandler:NO];
NSAffineTransform* transform1 = [NSAffineTransform transform];
[transform1 translateXBy:+(NSWidth(rotatedBounds) / 2) yBy:+(NSHeight(rotatedBounds) / 2)];
[transform1 rotateByDegrees:-1.0 * degrees];
[transform1 translateXBy:-(NSWidth(rotatedBounds) / 2) yBy:-(NSHeight(rotatedBounds) / 2)];
[rotatedImage lockFocus] ;
[transform1 concat] ;
[sourceImage drawInRect:imageBounds
fromRect:NSMakeRect(0.0, 0.0, [sourceImage size].width, [sourceImage size].height)
operation:NSCompositeSourceOver fraction:1.0];
[rotatedImage unlockFocus] ;
return rotatedImage;
}
}

Rotate an NSImage by arbitrary angle and supporting Retina displays

I need to rotate an NSImage by an arbitrary angle. For the iOS version of my app, I hacked together the following solution from several sources I found on the internets:
#implementation UIImage (ut_rotate)
- (instancetype)imageRotatedByDegrees:(CGFloat)degrees{
CGFloat scale = [UIScreen mainScreen].scale; // support retina displays
float newSide = self.size.width; // square img
CGSize size = CGSizeMake(newSide, newSide);
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, newSide/2.0f, newSide/2.0f);
transform = CGAffineTransformRotate(transform, _heading * M_PI/180.0);
transform = CGAffineTransformScale(transform, 1.0, -1.0);
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(-newSide/2.0f, -newSide/2.0f, newSide, newSide), _image.CGImage);
return [UIImage imageWithCGImage: CGBitmapContextCreateImage(context) scale:scale orientation:UIImageOrientationUp];
}
#end
Note that this hack supports retina display.
For Cocoa, I found the canonical solution on one of the internets:
#implementation NSImage (ut_rotate)
- (NSImage*)imageRotatedByDegrees:(CGFloat)degrees {
// Calculate the bounds for the rotated image
// We do this by affine-transforming the bounds rectangle
NSRect imageBounds = {NSZeroPoint, [self size]};
NSBezierPath* boundsPath = [NSBezierPath bezierPathWithRect:imageBounds];
NSAffineTransform* transform = [NSAffineTransform transform];
[transform rotateByDegrees:-1.0 * degrees];// we want clockwise angles
[boundsPath transformUsingAffineTransform:transform];
NSRect rotatedBounds = {NSZeroPoint, [boundsPath bounds].size};
NSImage* rotatedImage = [[NSImage alloc] initWithSize:rotatedBounds.size] ;
// Center the image within the rotated bounds
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2);
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2);
// Start a new transform, to transform the image
transform = [NSAffineTransform transform];
// Move coordinate system to the center
// (since we want to rotate around the center)
[transform translateXBy:+(NSWidth(rotatedBounds) / 2)
yBy:+(NSHeight(rotatedBounds) / 2)];
// Do the rotation
[transform rotateByDegrees:-1.0 * degrees];
// Move coordinate system back to normal (bottom, left)
[transform translateXBy:-(NSWidth(rotatedBounds) / 2)
yBy:-(NSHeight(rotatedBounds) / 2)];
// Draw the original image, rotated, into the new image
// Note: This "drawing" is done off-screen.
[rotatedImage lockFocus];
[transform concat];
[self drawInRect:imageBounds fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0] ;
[rotatedImage unlockFocus];
return rotatedImage;
}
#end
Problem is: a) results look blurry already on non-retina displays b) doesn't support retina.
I found something about substituting code between lockFocus and unlockFocus with block-based drawing methods but don't understand it.
I'm looking for a solution that produces sharper images and supports retina. For my needs only square source images have to be supported on OS X 10.9 and later.
This is completely untested but may work:
#implementation NSImage (ut_rotate)
- (NSImage*)imageRotatedByDegrees:(CGFloat)degrees {
// Calculate the bounds for the rotated image
// We do this by affine-transforming the bounds rectangle
NSRect imageBounds = {NSZeroPoint, [self size]};
NSBezierPath* boundsPath = [NSBezierPath bezierPathWithRect:imageBounds];
NSAffineTransform* transform = [NSAffineTransform transform];
[transform rotateByDegrees:-1.0 * degrees];// we want clockwise angles
[boundsPath transformUsingAffineTransform:transform];
NSRect rotatedBounds = {NSZeroPoint, [boundsPath bounds].size};
// Center the image within the rotated bounds
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2);
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2);
NSImage* rotatedImage = [NSImage imageWithSize:rotatedBounds.size flipped:NO drawingHandler:^BOOL (NSRect dstRect){
// Start a new transform, to transform the image
NSAffineTransform* transform = [NSAffineTransform transform];
// Move coordinate system to the center
// (since we want to rotate around the center)
[transform translateXBy:+(NSWidth(rotatedBounds) / 2)
yBy:+(NSHeight(rotatedBounds) / 2)];
// Do the rotation
[transform rotateByDegrees:-1.0 * degrees];
// Move coordinate system back to normal (bottom, left)
[transform translateXBy:-(NSWidth(rotatedBounds) / 2)
yBy:-(NSHeight(rotatedBounds) / 2)];
// Draw the original image, rotated, into the new image
[transform concat];
[self drawInRect:imageBounds fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
return YES;
}];
return rotatedImage;
}
#end
My extension for NSImage, work properly with all degrees
- (NSImage *)rotateByDegrees:(CGFloat)degrees
{
NSImage *image = nil;
if (degrees == 0.0)
{
image = self;
}
else
{
CIImage *ciImage = [[[CIImage alloc] initWithData:self.TIFFRepresentation] autorelease];
CGFloat angle = degrees * M_PI / 180.0;
CIImage *output = [ciImage imageByApplyingTransform:CGAffineTransformMakeRotation(angle)];
image = [[[NSImage alloc] initWithSize:NSMakeSize(self.size.width * fabs(cos(angle)) + self.size.height * fabs(sin(angle)), self.size.width * fabs(sin(angle)) + self.size.height * fabs(cos(angle)))] autorelease];
[image addRepresentation:[NSCIImageRep imageRepWithCIImage:output]];
}
return image;
}

reset image in tableView imageCell is blurry

i'm trying to show an 'info' icon during a cursor 'rollover' on an NSTableView cell. i'm getting a copy of the cell's image, drawing the 'info' icon on it, and telling the cell to setImage with this copy. just drawing the icon will scale it and it won't be the same size in every one as the images in the table are different sizes. i'm not having a problem with the scaling or positioning the correct size icon.
my problem is that the replaced image is slightly blurry and it's edges are not crisp on close examination. the lack of edge makes it appear to be 'moving' slightly when the mouseEntered happens and the image is replaced.
i've tried a number of drawing techniques that doen't use lockFocus on an NSImage (drawing in CGBitmapContext, or using CIFilter compositing), and they produce the same results.
i'm using NSTableView's preparedCellAtColumn as it seems that drawing in willDisplayCell is unpredictable -- i read somewhere but can't remember where.
here is my preparedCellAtColumn method:
- (NSCell *)preparedCellAtColumn:(NSInteger)column row:(NSInteger)row
{
NSCell *cell = [super preparedCellAtColumn:column row:row];
if ((self.mouseOverRow == row) && column == 0) {
NSCell * imageCell = [super preparedCellAtColumn:0 row:row];
NSImage *sourceImage = [[imageCell image] copy];
NSRect cellRect = [self frameOfCellAtColumn:0 row:row];
NSSize cellSize = cellRect.size;
NSSize scaledSize = [sourceImage proportionalSizeForTargetSize:cellSize];
NSImage *outputImage = [[NSImage alloc] initWithSize:cellSize];
[outputImage setBackgroundColor:[NSColor clearColor]];
NSPoint drawPoint = NSZeroPoint;
drawPoint.x = (cellSize.width - scaledSize.width) * 0.5;
drawPoint.y = (cellSize.height - scaledSize.height) * 0.5;
NSRect drawRect = NSMakeRect(drawPoint.x, drawPoint.y, scaledSize.width, scaledSize.height);
NSPoint infoPoint = drawPoint;
infoPoint.x += NSWidth(drawRect) - self.infoSize.width;
[outputImage lockFocus];
[sourceImage drawInRect:drawRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
[self.infoImage drawAtPoint:infoPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
[outputImage unlockFocus];
[cell setImage:outputImage];
}
return cell;
}
[this is the enclosed scaling method from scott stevenson]
- (NSSize)proportionalSizeForTargetSize:(NSSize)targetSize
{
NSSize imageSize = [self size];
CGFloat width = imageSize.width;
CGFloat height = imageSize.height;
CGFloat targetWidth = targetSize.width;
CGFloat targetHeight = targetSize.height;
CGFloat scaleFactor = 0.0;
CGFloat scaledWidth = targetWidth;
CGFloat scaledHeight = targetHeight;
if ( NSEqualSizes( imageSize, targetSize ) == NO )
{
CGFloat widthFactor;
CGFloat heightFactor;
widthFactor = targetWidth / width;
heightFactor = targetHeight / height;
if ( widthFactor < heightFactor )
scaleFactor = widthFactor;
else
scaleFactor = heightFactor;
scaledWidth = width * scaleFactor;
scaledHeight = height * scaleFactor;
}
return NSMakeSize(scaledWidth,scaledHeight);
}
i need to support 10.6, so can't use new groovy lion methods.
thanks for your consideration...
Your code is setting the origin of the image to non-integral coordinates. That means that the edge of the image will not be pixel-aligned, producing a fuzzy result.
You just need to do this:
NSPoint drawPoint = NSZeroPoint;
drawPoint.x = round((cellSize.width - scaledSize.width) * 0.5);
drawPoint.y = round((cellSize.height - scaledSize.height) * 0.5);
That will ensure the image origin has no fractional component.

How to draw a blurred shape?

How do I draw a blurred shape in Cocoa? Think of a shadow with a blurRadius accompanying a filled path, but without sharp-edged foreground path shape.
What I tried is using a filled path with a shadow, and setting the fill color to transparent (alpha 0.0). But that makes the shadow invisible as well, as it is apparently taking the shadow casting "object's" alpha into account.
This is actually reasonably tricky. I struggled with this for a while until I came up with this category on NSShadow:
#implementation NSShadow (Extras)
//draw a shadow using a bezier path but do not draw the bezier path
- (void)drawUsingBezierPath:(NSBezierPath*) path alpha:(CGFloat) alpha
{
[NSGraphicsContext saveGraphicsState];
//get the bounds of the path
NSRect bounds = [path bounds];
//create a rectangle that outsets the size of the path bounds by the blur radius amount
CGFloat blurRadius = [self shadowBlurRadius];
NSRect shadowBounds = NSInsetRect(bounds, -blurRadius, -blurRadius);
//create an image to hold the shadow
NSImage* shadowImage = [[NSImage alloc] initWithSize:shadowBounds.size];
//make a copy of the shadow and set its offset so that when the path is drawn, the shadow is drawn in the middle of the image
NSShadow* aShadow = [self copy];
[aShadow setShadowOffset:NSMakeSize(0, -NSHeight(shadowBounds))];
//lock focus on the image
[shadowImage lockFocus];
//we want to draw the path directly above the shadow image and offset the shadow so it is drawn in the image rect
//to do this we must translate the drawing into the correct location
NSAffineTransform* transform=[NSAffineTransform transform];
//first get it to the zero point
[transform translateXBy:-shadowBounds.origin.x yBy:-shadowBounds.origin.y];
//now translate it by the height of the image so that it draws outside the image bounds
[transform translateXBy:0.0 yBy:NSHeight(shadowBounds)];
NSBezierPath* translatedPath = [transform transformBezierPath:path];
//apply the shadow
[aShadow set];
//fill the path with an arbitrary black color
[[NSColor blackColor] set];
[translatedPath fill];
[aShadow release];
[shadowImage unlockFocus];
//draw the image at the correct location relative to the original path
NSPoint imageOrigin = bounds.origin;
imageOrigin.x = (imageOrigin.x - blurRadius) + [self shadowOffset].width;
imageOrigin.y = (imageOrigin.y - blurRadius) - [self shadowOffset].height;
[shadowImage drawAtPoint:imageOrigin fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:alpha];
[shadowImage release];
[NSGraphicsContext restoreGraphicsState];
}
#end
I found one implementation which does exactly what I meant online. Written by Sean Patrick O'Brien. It is a category on NSBezierPath, like so:
#interface NSBezierPath (MCAdditions)
- (void)drawBlurWithColor:(NSColor *)color radius:(CGFloat)radius;
#end
#implementation NSBezierPath (MCAdditions)
- (void)drawBlurWithColor:(NSColor *)color radius:(CGFloat)radius
{
NSRect bounds = NSInsetRect(self.bounds, -radius, -radius);
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowOffset = NSMakeSize(0, bounds.size.height);
shadow.shadowBlurRadius = radius;
shadow.shadowColor = color;
NSBezierPath *path = [self copy];
NSAffineTransform *transform = [NSAffineTransform transform];
if ([[NSGraphicsContext currentContext] isFlipped])
[transform translateXBy:0 yBy:bounds.size.height];
else
[transform translateXBy:0 yBy:-bounds.size.height];
[path transformUsingAffineTransform:transform];
[NSGraphicsContext saveGraphicsState];
[shadow set];
[[NSColor blackColor] set];
NSRectClip(bounds);
[path fill];
[NSGraphicsContext restoreGraphicsState];
[path release];
[shadow release];
}
#end
This code is downloadable from this blog post. I didn't find it separately as code anywhere online.

Image inside NSScrollView drawing on top of other views

I have a custom NSView that lives inside of a NSScrollView that is in a NSSplitView. The custom view uses the following drawing code:
- (void)drawRect:(NSRect)dirtyRect {
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
[ctx saveGraphicsState];
// Rounded Rect
NSRect rect = [self bounds];
NSRect pathRect = NSMakeRect(rect.origin.x + 3, rect.origin.y + 6, rect.size.width - 6, rect.size.height - 6);
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:pathRect cornerRadius:kDefaultCornerRadius];
// Shadow
[NSShadow setShadowWithColor:[NSColor colorWithCalibratedWhite:0 alpha:0.66]
blurRadius:4.0
offset:NSMakeSize(0, -3)];
[[NSColor colorWithCalibratedWhite:0.196 alpha:1.0] set];
[path fill];
[NSShadow clearShadow];
// Background Gradient
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[UAColor darkBlackColor] endingColor:[UAColor lightBlackColor]];
[gradient drawInBezierPath:path angle:90.0];
[gradient release];
// Image
[path setClip];
NSRect imageRect = NSMakeRect(pathRect.origin.x, pathRect.origin.y, pathRect.size.height * kLargeImageRatio, pathRect.size.height);
[self.image drawInRect:imageRect
fromRect:NSZeroRect
operation:NSCompositeSourceAtop
fraction:1.0];
[ctx restoreGraphicsState];
[super drawRect:dirtyRect];
}
I have tried every different type of operation but the image still draws on top of the other half of the NSSplitView like so:
…instead of drawing under the NSScrollView. I think this has to do with drawing everything instead of the dirtyRect only, but I don't know how I could edit the image drawing code to only draw the part of it that lies in the dirtyRect. How can I either prevent it from drawing on top, or only draw the dirty rect for this NSImage?
I finally got it. I don't know if it is optimal yet, but I will find out when doing performance testing. I just had to figure out the intersection of the image rect and the dirty rect using NSIntersectionRect, then figuring out which subpart of the NSImage to draw for the drawInRect:fromRect:operation:fraction: call.
Here is the important part:
NSRect imageRect = NSMakeRect(pathRect.origin.x, pathRect.origin.y, pathRect.size.height * kLargeImageRatio, pathRect.size.height);
[self.image setSize:imageRect.size];
NSRect intersectionRect = NSIntersectionRect(dirtyRect, imageRect);
NSRect fromRect = NSMakeRect(intersectionRect.origin.x - imageRect.origin.x,
intersectionRect.origin.y - imageRect.origin.y,
intersectionRect.size.width,
intersectionRect.size.height);
[self.image drawInRect:intersectionRect
fromRect:fromRect
operation:NSCompositeSourceOver
fraction:1.0];

Resources