How to save a PNG file from NSImage (retinal problems)

I do some operations on images, and after I finish, I want to save the image as PNG to disk. I do the following:

+ (void)saveImage:(NSImage *)image atPath:(NSString *)path { [image lockFocus] ; NSBitmapImageRep *imageRepresentation = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0.0, 0.0, image.size.width, image.size.height)] ; [image unlockFocus] ; NSData *data = [imageRepresentation representationUsingType:NSPNGFileType properties:nil]; [data writeToFile:path atomically:YES]; } 

This code works, but the problem is with the mac retina, if I print the NSBitmapImageRep object, I get a different size and a straight pixel, and when my image is saved to disk, it is twice as large:

 $0 = 0x0000000100413890 NSBitmapImageRep 0x100413890 Size={300, 300} ColorSpace=sRGB IEC61966-2.1 colorspace BPS=8 BPP=32 Pixels=600x600 Alpha=YES Planar=NO Format=0 CurrentBacking=<CGImageRef: 0x100414830> 

I tied to make the pixel size not care about the scale of the retina, since I want to keep the original size:

 imageRepresentation.pixelsWide = image.size.width; imageRepresentation.pixelsHigh = image.size.height; 

This time I get the correct size when I print the NSBitmapImageRep object, but when I save the file, I still get the same problem:

 $0 = 0x0000000100413890 NSBitmapImageRep 0x100413890 Size={300, 300} ColorSpace=sRGB IEC61966-2.1 colorspace BPS=8 BPP=32 Pixels=300x300 Alpha=YES Planar=NO Format=0 CurrentBacking=<CGImageRef: 0x100414830> 

Any idea how to fix this and keep the original pixel size?

+26
retina-display image-resizing macos nsimage nsbitmapimagerep
Jul 06 '13 at 21:08
source share
6 answers

If you have NSImage and want to save it as an image file in the file system, you should never use lockFocus ! lockFocus creates a new image that is defined to display the screen and nothing else. Therefore, lockFocus uses the screen properties: 72 dpi for regular screens and 144 dpi for retina screens. Why do I want to offer the following code:

 + (void)saveImage:(NSImage *)image atPath:(NSString *)path { CGImageRef cgRef = [image CGImageForProposedRect:NULL context:nil hints:nil]; NSBitmapImageRep *newRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef]; [newRep setSize:[image size]]; // if you want the same resolution NSData *pngData = [newRep representationUsingType:NSPNGFileType properties:nil]; [pngData writeToFile:path atomically:YES]; [newRep autorelease]; } 
+42
Jul 07 '13 at 8:58
source share

NSImage has permission and uses the HiDPI graphics context when you lockFocus on a retina system.
The dimensions of the image that you pass to your NSBitmapImageRep initializer are in pixels (not pixels). Thus, an image with a width of 150.0 pixels uses 300 horizontal pixels in a @ 2x context.

You can use convertRectToBacking: or backingScaleFactor: to compensate for @ 2x context. (I did not), or you can use the following NSImage category, which creates a drawing context with explicit pixel sizes:

 @interface NSImage (SSWPNGAdditions) - (BOOL)writePNGToURL:(NSURL*)URL outputSizeInPixels:(NSSize)outputSizePx error:(NSError*__autoreleasing*)error; @end @implementation NSImage (SSWPNGAdditions) - (BOOL)writePNGToURL:(NSURL*)URL outputSizeInPixels:(NSSize)outputSizePx error:(NSError*__autoreleasing*)error { BOOL result = YES; NSImage* scalingImage = [NSImage imageWithSize:[self size] flipped:NO drawingHandler:^BOOL(NSRect dstRect) { [self drawAtPoint:NSMakePoint(0.0, 0.0) fromRect:dstRect operation:NSCompositeSourceOver fraction:1.0]; return YES; }]; NSRect proposedRect = NSMakeRect(0.0, 0.0, outputSizePx.width, outputSizePx.height); CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); CGContextRef cgContext = CGBitmapContextCreate(NULL, proposedRect.size.width, proposedRect.size.height, 8, 4*proposedRect.size.width, colorSpace, kCGBitmapByteOrderDefault|kCGImageAlphaPremultipliedLast); CGColorSpaceRelease(colorSpace); NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:NO]; CGContextRelease(cgContext); CGImageRef cgImage = [scalingImage CGImageForProposedRect:&proposedRect context:context hints:nil]; CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)(URL), kUTTypePNG, 1, NULL); CGImageDestinationAddImage(destination, cgImage, nil); if(!CGImageDestinationFinalize(destination)) { NSDictionary* details = @{NSLocalizedDescriptionKey:@"Error writing PNG image"}; [details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain:@"SSWPNGAdditionsErrorDomain" code:10 userInfo:details]; result = NO; } CFRelease(destination); return result; } @end 
+15
Jul 07 '13 at 8:00
source share

I found this code on the Internet and it works on the retina. Paste here, hope someone can help.

 NSImage *computerImage = [NSImage imageNamed:NSImageNameComputer]; NSInteger size = 256; NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:size pixelsHigh:size bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bytesPerRow:0 bitsPerPixel:0]; [rep setSize:NSMakeSize(size, size)]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:rep]]; [computerImage drawInRect:NSMakeRect(0, 0, size, size) fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0]; [NSGraphicsContext restoreGraphicsState]; NSData *data = [rep representationUsingType:NSPNGFileType properties:nil]; 
+5
Mar 20 '15 at 8:44
source share

Just expose someone on this subject. Here, of course, is the drawback of a solution that performs the task of saving a 1x image (image.size) regardless of the device in swift

 public func writeToFile(path: String, atomically: Bool = true) -> Bool{ let bitmap = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(self.size.width), pixelsHigh: Int(self.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSDeviceRGBColorSpace, bytesPerRow: 0, bitsPerPixel: 0)! bitmap.size = self.size NSGraphicsContext.saveGraphicsState() NSGraphicsContext.setCurrentContext(NSGraphicsContext(bitmapImageRep: bitmap)) self.drawAtPoint(CGPoint.zero, fromRect: NSRect.zero, operation: NSCompositingOperation.CompositeSourceOver, fraction: 1.0) NSGraphicsContext.restoreGraphicsState() if let imagePGNData = bitmap.representationUsingType(NSBitmapImageFileType.NSPNGFileType, properties: [NSImageCompressionFactor: 1.0]) { return imagePGNData.writeToFile((path as NSString).stringByStandardizingPath, atomically: atomically) } else { return false } } 
+4
Aug 19 '15 at 21:41
source share

My 2 cents for OS X, including recording, process extensions + image screen (method 2); can be checked with NSGraphicsContext.currentContextDrawingToScreen ()

 func createCGImage() -> CGImage? { //method 1 let image = NSImage(size: NSSize(width: bounds.width, height: bounds.height), flipped: true, drawingHandler: { rect in self.drawRect(self.bounds) return true }) var rect = CGRectMake(0, 0, bounds.size.width, bounds.size.height) return image.CGImageForProposedRect(&rect, context: bitmapContext(), hints: nil) //method 2 if let pdfRep = NSPDFImageRep(data: dataWithPDFInsideRect(bounds)) { return pdfRep.CGImageForProposedRect(&rect, context: bitmapContext(), hints: nil) } return nil } func PDFImageData(filter: QuartzFilter?) -> NSData? { return dataWithPDFInsideRect(bounds) } func bitmapContext() -> NSGraphicsContext? { var context : NSGraphicsContext? = nil if let imageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(bounds.size.width), pixelsHigh: Int(bounds.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace, bytesPerRow: Int(bounds.size.width) * 4, bitsPerPixel: 32) { imageRep.size = NSSize(width: bounds.size.width, height: bounds.size.height) context = NSGraphicsContext(bitmapImageRep: imageRep) } return context } func writeImageData(view: MyView, destination: NSURL) { if let dest = CGImageDestinationCreateWithURL(destination, imageUTType, 1, nil) { let properties = imageProperties let image = view.createCGImage()! let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) dispatch_async(queue) { CGImageDestinationAddImage(dest, image, properties) CGImageDestinationFinalize(dest) } } } 
0
Sep 13 '16 at 2:04 on
source share

Here is a version of Swift 5 based on Heinrich Giessen's answer :

 static func saveImage(_ image: NSImage, atUrl url: URL) { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return } // TODO: handle error let newRep = NSBitmapImageRep(cgImage: cgImage) newRep.size = image.size // if you want the same size guard let pngData = newRep.representation(using: .png, properties: [:]) else { return } // TODO: handle error do { try pngData.write(to: url) } catch { print("error saving: \(error)") } } 
0
May 6 '19 at
source share



All Articles