9

Is there any way in Objective-c/cocoa (OSX) to crop an image without changing the quality of the image?

I am very near to a solution, but there are still some differences that I can detect in the color. I can notice it when zooming into the text. Here is the code I am currently using:

    NSImage *target = [[[NSImage alloc]initWithSize:panelRect.size] autorelease];
    target.backgroundColor = [NSColor greenColor];
    //start drawing on target
    [target lockFocus];
    [NSGraphicsContext saveGraphicsState];
    [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationNone];
    [[NSGraphicsContext currentContext] setShouldAntialias:NO];

    //draw the portion of the source image on target image
    [source drawInRect:NSMakeRect(0,0,panelRect.size.width,panelRect.size.height)
              fromRect:NSMakeRect(panelRect.origin.x , source.size.height - panelRect.origin.y - panelRect.size.height, panelRect.size.width, panelRect.size.height)
             operation:NSCompositeCopy
              fraction:1.0];

    [NSGraphicsContext restoreGraphicsState];
    //end drawing
    [target unlockFocus];

    //create a NSBitmapImageRep
    NSBitmapImageRep *bmpImageRep = [[[NSBitmapImageRep alloc]initWithData:[target TIFFRepresentation]] autorelease];
    //add the NSBitmapImage to the representation list of the target
    [target addRepresentation:bmpImageRep];
    //get the data from the representation
    NSData *data = [bmpImageRep representationUsingType: NSJPEGFileType
                                             properties: imageProps];
    NSString *filename = [NSString stringWithFormat:@"%@%@.jpg", panelImagePrefix, panelNumber];
    NSLog(@"This is the filename: %@", filename);
    //write the data to a file
    [data writeToFile:filename atomically:NO];

Here is a zoomed-in comparison of the original and the cropped image:

The Original Image (Original image - above)

The Cropped Image (Cropped image - above)

The difference is hard to see, but if you flick between them, you can notice it. You can use a colour picker to notice the difference as well. For example, the darkest pixel on the bottom row of the image is a different shade.

I also have a solution that works exactly the way I want it in iOS. Here is the code:

-(void)testMethod:(int)page forRect:(CGRect)rect{
    NSString *filePath = @"imageName";

    NSData *data = [HeavyResourceManager dataForPath:filePath];//this just gets the image as NSData
    UIImage *image = [UIImage imageWithData:data];

    CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], rect);//crop in the rect

    UIImage *result = [UIImage imageWithCGImage:imageRef scale:0 orientation:image.imageOrientation];
    CGImageRelease(imageRef);

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectoryPath = [paths objectAtIndex:0];

    [UIImageJPEGRepresentation(result, 1.0) writeToFile:[documentsDirectoryPath stringByAppendingPathComponent::@"output.jpg"] atomically:YES];
}

So, is there a way to crop an image in OSX so that the cropped image does not change at all? Perhaps I have to look into a different library, but I would be surprised if I could not do this with Objective-C...


Note, This is a follow up question to my previous question here.


Update I have tried (as per the suggestion) to round the CGRect values to whole numbers, but did not notice a difference. Here is the code in case I used:

    [source drawInRect:NSMakeRect(0,0,(int)panelRect.size.width,(int)panelRect.size.height)
              fromRect:NSMakeRect((int)panelRect.origin.x , (int)(source.size.height - panelRect.origin.y - panelRect.size.height), (int)panelRect.size.width, (int)panelRect.size.height)
             operation:NSCompositeCopy
              fraction:1.0];

Update I have tried mazzaroth code and it works if I save it as a png, but if I try and save it as a jpeg, the image loses quality. So close, but not close enough. Still hoping for a complete answer...

Community
  • 1
  • 1
lindon fox
  • 3,298
  • 3
  • 33
  • 59
  • Do you still see the problem if you make a TIFF representation instead of JPEG? – user1118321 Dec 05 '12 at 05:42
  • Hi @user1118321, Yes. I have already tested with TIFF and with PNG. Same outcome. – lindon fox Dec 05 '12 at 06:20
  • Are you zoomed in so that you have a 1:1 pixel to screen ratio before you crop? – lnafziger Dec 05 '12 at 06:23
  • @lnafziger, I did not think that there is any zooming involved... but I could be missing something. I am creating the source image with 'initWithContentsOfFile'. There is no modification to the image after that... – lindon fox Dec 05 '12 at 06:26
  • Well, it's just a thought. If they are scaled differently then it would create different values for different pixels... Are you using auto layout or auto-sizing that might resize the image? – lnafziger Dec 05 '12 at 06:29
  • @Inafziger, I am just viewing the images in Finder. The code above is all I am using to save the cropped image... It is interesting. All the pixels seem to be in the right place, but the colour is just a bit off. – lindon fox Dec 05 '12 at 06:36
  • did you try to disable/enable antialiasing on the image context? – Cocoanetics Dec 15 '12 at 22:45
  • @Cocoanetics, Yes. `[[NSGraphicsContext currentContext] setShouldAntialias:NO];` – lindon fox Dec 17 '12 at 02:09
  • Did you try offsetting the CTM by (0.5,0.5). Quartz otherwise distributes the pixel color otherwise to the adjacent 4 pixels. – Cocoanetics Dec 17 '12 at 06:06
  • Can you please accept an answer so that the bounty gets applied? – Cocoanetics Dec 17 '12 at 11:57

4 Answers4

12

use CGImageCreateWithImageInRect.

// this chunk of code loads a jpeg image into a cgimage
// creates a second crop of the original image with CGImageCreateWithImageInRect
// writes the new cropped image to the desktop
// ensure that the xy origin of the CGRectMake call is smaller than the width or height of the original image

NSURL *originalImage = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"lockwood" ofType:@"jpg"]];
CGImageRef imageRef = NULL;

CGImageSourceRef loadRef = CGImageSourceCreateWithURL((CFURLRef)originalImage, NULL);
if (loadRef != NULL)
{
    imageRef = CGImageSourceCreateImageAtIndex(loadRef, 0, NULL);
    CFRelease(loadRef); // Release CGImageSource reference
}    
CGImageRef croppedImage = CGImageCreateWithImageInRect(imageRef, CGRectMake(200., 200., 100., 100.));

CFURLRef saveUrl = (CFURLRef)[NSURL fileURLWithPath:[@"~/Desktop/lockwood-crop.jpg" stringByExpandingTildeInPath]];
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(saveUrl, kUTTypeJPEG, 1, NULL);
CGImageDestinationAddImage(destination, croppedImage, nil);

if (!CGImageDestinationFinalize(destination)) {
    NSLog(@"Failed to write image to %@", saveUrl);
}

CFRelease(destination);
CFRelease(imageRef);
CFRelease(croppedImage);

I also made a gist:

https://gist.github.com/4259594

maz
  • 8,056
  • 4
  • 26
  • 25
  • Hi, Thanks for the answer. I have implemented your suggestion, but it is still not perfect. The colour seems better, but some pixels are moving around still, as if it is doing some anti aliasing. I will look into it further. – lindon fox Dec 12 '12 at 02:03
  • So after a bit of playing around, it seems it is the saving of the file that causes the problem. If I change it to PNG format, the image is perfect... So now I just need to know how to stop the save process of the jpg from destroying my hard work. – lindon fox Dec 12 '12 at 02:48
  • 2
    JPEG by design is "lossy". I'm afraid your efforts to not have a change in colour when outputting to file will be fruitless. – maz Dec 13 '12 at 16:09
  • @ mazzaroth, The iOS code that I have in the question (see above) works without changing the image. If the image data is from a jpeg image and I am saving a cropped version of that image I should be able to save the new image without re-compressing and losing quality. – lindon fox Dec 14 '12 at 01:25
  • @lindon, JPEG is a lossy compression format. You will always lose quality going from bitmap representation ~> JPEG. Save it in a different format if you don't want to lose bits. – maz Dec 14 '12 at 16:58
  • @ mazzaroth, thanks for following up on this. My question is how to get jpeg->jpeg crop without losing quality. I up-voted because you came close. You seem to be saying that you can not do this, because JPEG is lossy. But there is no reason why, if you have a jpeg image you would _have_ to run the compression algorithm again. Of course, there would be techniques that require this, but that would mean it would not be a proper crop. You *can* do it because I did it in the iOS code (above); it was not a perfect crop, but there was none of the artifacts that are the hallmark of JPEG compression. – lindon fox Dec 17 '12 at 02:08
  • You can specify the compression level when saving to JPEG. Try with something like 0.8 or 0.9. you can also save JPEG without compression with 1.0, but ther PNG has a distinct advantage. You specify the compression level in the options dictionary for CGImageDesinationAddImage. – Cocoanetics Dec 17 '12 at 07:43
  • @lindonfox there is your answer, try the CGImageCreateWithImageInRect technique and use compression 1.0 when saving it to a jpeg file. Do I get my bounty now? :) – maz Dec 17 '12 at 14:13
  • @mazzaroth, ha ha. That does not fix it. I have always been using a compression rate of `1.0`. But since you asked so nicely... (actually, you have been pretty helpful, so I will give you the bounty, but I won't mark this question as solved.) `bounty give` – lindon fox Dec 17 '12 at 15:57
  • @lindonfox thanks lindon, I'm glad to help. I appreciate it, you've got honour; hard to find that these days. – maz Dec 17 '12 at 19:02
3

Try to change the drawInRect orign to 0.5,0.5. Otherwise Quartz will distribute each pixel color to the adjacent 4 fixels.

Set the color space of the target image. You might be having a different colorspace causing to to look slightly different.

Try the various rendering intents and see which gets the best result, perceptual versus relative colorimetric etc. There are 4 options I think.

You mention that the colors get modified by the saving of JPEG versus PNG.

You can specify the compression level when saving to JPEG. Try with something like 0.8 or 0.9. you can also save JPEG without compression with 1.0, but ther PNG has a distinct advantage. You specify the compression level in the options dictionary for CGImageDesinationAddImage.

Finally - if nothing her helps - you should open a TSI with DTS, they can certainly provide you with the guidance you seek.

Cocoanetics
  • 8,171
  • 2
  • 30
  • 57
  • Thanks for the suggestion, but it did not seem to make any difference. This is the code I changed: `[source drawInRect:NSMakeRect(0.5,0.5,roundedPanelRect.size.width,roundedPanelRect.size.height) fromRect:roundedPanelRect operation:NSCompositeCopy fraction:1.0];` – lindon fox Dec 17 '12 at 07:03
  • Try to replace the fromRect with NSZeroRect, that takes the entire area of the source image – Cocoanetics Dec 17 '12 at 07:05
  • The `fromRect` is the part that I am cropping. If I put in zero, it just shrinks the whole image... I did try setting both the `drawInRect` and `fromRect` origin values to be `-.5` values with no change. – lindon fox Dec 17 '12 at 07:21
  • I see. Could it be that parts of your image are slighly transparent? Also the last thing I can think of is that you probably need to fiddle with the color space and rendering intent since you are on Mac. I also added these suggestions to my answer. – Cocoanetics Dec 17 '12 at 07:24
  • No, the image has no transparent parts. Perhaps it is something to do with colour space/rendering intent... I did look through the different methods that might affect that, but I should have another look through. I will follow your suggestions in your answer. – lindon fox Dec 17 '12 at 07:27
2

The usual problem is that cropping sizes are float, but image pixels are integer. cocoa interpolates it automatically.

You need to floor, round or ceil the size and coordinates to be sure that they are integer.

This may help.

Remizorrr
  • 2,322
  • 1
  • 17
  • 25
  • Thanks for your suggestion. I believe I tried this earlier. I tried it again, but no change. See the update in the answer. cheers – lindon fox Dec 11 '12 at 09:33
0

I am doing EXIF deleting of JPG files and I think I caught the reason:

All losses and changes come from the re-compress of your image during saving to file.

You may notice the change too if you just to save the whole image again. What I am to do is to read the original JPG and re-compress it to a quality that take equivalent file size.

Jiulong Zhao
  • 1,313
  • 1
  • 15
  • 25