How To Save PNG file From NSImage (retina issues) The Right Way?

孤街浪徒 提交于 2019-12-05 10:36:57
tad

I had trouble with the answer provided in original thread too. Further reading landed me on a post by Erica Sadun related to debugging code for retina displays without a retina display. She creates a bitmap of the desired size, then replaces the current drawing context (display based/retina influenced) with the generic one associated with the new bitmap. She then renders the original image into the bitmap (using the generic graphics context).

I took her code and made a quick category on NSImage which seems to do the job for me. After calling

NSBitmapImageRep *myRep = [myImage unscaledBitmapImageRep];

you should have a bitmap of the proper (original) dimensions, regardless of the type of physical display you started with. From this point, you can call representationUsingType:properties on the unscaled bitmap to get whatever format you are looking to write out.

Here is my category (header omitted). Note - you may need to expose the colorspace portion of the bitmap initializer. This is the value that works for my particular case.

-(NSBitmapImageRep *)unscaledBitmapImageRep {

    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
                               initWithBitmapDataPlanes:NULL
                                             pixelsWide:self.size.width
                                             pixelsHigh:self.size.height
                                          bitsPerSample:8
                                        samplesPerPixel:4
                                               hasAlpha:YES
                                               isPlanar:NO
                                         colorSpaceName:NSDeviceRGBColorSpace
                                            bytesPerRow:0
                                           bitsPerPixel:0];
    rep.size = self.size;

   [NSGraphicsContext saveGraphicsState];
   [NSGraphicsContext setCurrentContext:
            [NSGraphicsContext graphicsContextWithBitmapImageRep:rep]];

    [self drawAtPoint:NSMakePoint(0, 0) 
             fromRect:NSZeroRect 
            operation:NSCompositeSourceOver 
             fraction:1.0];

    [NSGraphicsContext restoreGraphicsState];
    return rep;
}

Thank tad & SnowPaddler.

For anyone who is not familiar with Cocoa and using Swift 4, you can view Swift 2 & Swift 3 version from edit history:

import Cocoa

func unscaledBitmapImageRep(forImage image: NSImage) -> NSBitmapImageRep {
    guard let rep = NSBitmapImageRep(
        bitmapDataPlanes: nil,
        pixelsWide: Int(image.size.width),
        pixelsHigh: Int(image.size.height),
        bitsPerSample: 8,
        samplesPerPixel: 4,
        hasAlpha: true,
        isPlanar: false,
        colorSpaceName: .deviceRGB,
        bytesPerRow: 0,
        bitsPerPixel: 0
        ) else {
            preconditionFailure()
    }

    NSGraphicsContext.saveGraphicsState()
    NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
    image.draw(at: .zero, from: .zero, operation: .sourceOver, fraction: 1.0)
    NSGraphicsContext.restoreGraphicsState()

    return rep
}

func writeImage(
    image: NSImage,
    usingType type: NSBitmapImageRep.FileType,
    withSizeInPixels size: NSSize?,
    to url: URL) throws {
    if let size = size {
        image.size = size
    }
    let rep = unscaledBitmapImageRep(forImage: image)

    guard let data = rep.representation(using: type, properties: [.compressionFactor: 1.0]) else {
        preconditionFailure()
    }

    try data.write(to: url)
}

Tad - thank you very much for this code - I agonised over this for days! It helped me write a file from a NSImage whilst keeping the resolution to 72DPI despite the retina display installed on my Mac. For the benefit of others that want to save a NSImage to a file with a specific pixel size and type (PNG, JPG etc) with a resolution of 72 DPI, here is the code that worked for me. I found that you need to set the size of your image before calling unscaledBitmapImageRep for this to work.

-(void)saveImage:(NSImage *)image
     AsImageType:(NSBitmapImageFileType)imageType
         forSize:(NSSize)targetSize
          atPath:(NSString *)path
{
    image.size = targetSize;

    NSBitmapImageRep * rep = [image unscaledBitmapImageRep:targetSize];

    // Write the target image out to a file
    NSDictionary *imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:1.0] forKey:NSImageCompressionFactor];
    NSData *targetData = [rep representationUsingType:imageType properties:imageProps];
    [targetData writeToFile:path atomically: NO];

    return;
}

I have also included the source code for the category header and .m file below.

The NSImage+Scaling.h file:

#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>

@interface NSImage (Scaling)

-(NSBitmapImageRep *)unscaledBitmapImageRep;

@end

And the NSImage+Scaling.m file:

#import "NSImage+Scaling.h"

#pragma mark - NSImage_Scaling
@implementation NSImage (Scaling)

-(NSBitmapImageRep *)unscaledBitmapImageRep
{

    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
                             initWithBitmapDataPlanes:NULL
                             pixelsWide:self.size.width
                             pixelsHigh:self.size.height
                             bitsPerSample:8
                             samplesPerPixel:4
                             hasAlpha:YES
                             isPlanar:NO
                             colorSpaceName:NSDeviceRGBColorSpace
                             bytesPerRow:0
                             bitsPerPixel:0];

    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:
    [NSGraphicsContext graphicsContextWithBitmapImageRep:rep]];    

    [self drawAtPoint:NSMakePoint(0, 0)
             fromRect:NSZeroRect
            operation:NSCompositeSourceOver
             fraction:1.0];

    [NSGraphicsContext restoreGraphicsState];
    return rep;
}

@end

I had the same difficulties with saving an NSImage object to a PNG or JPG file, and I finally understood why...

Firstly, the code excerpt shown above works well:

import Cocoa

func unscaledBitmapImageRep(forImage image: NSImage) -> NSBitmapImageRep {
    guard let rep = NSBitmapImageRep(
        bitmapDataPlanes: nil,
        pixelsWide: Int(image.size.width),
        pixelsHigh: Int(image.size.height),
        bitsPerSample: 8,
        samplesPerPixel: 4,
        hasAlpha: true,
        isPlanar: false,
        colorSpaceName: .deviceRGB,
        bytesPerRow: 0,
        bitsPerPixel: 0
    ) else {
        preconditionFailure()
    }

    NSGraphicsContext.saveGraphicsState()
    NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
    image.draw(at: .zero, from: .zero, operation: .sourceOver, fraction: 1.0)
    NSGraphicsContext.restoreGraphicsState()

    return rep
}

func writeImage(
    image: NSImage,
    usingType type: NSBitmapImageRep.FileType,
    withSizeInPixels size: NSSize?,
    to url: URL) throws {
    if let size = size {
        image.size = size
    }
    let rep = unscaledBitmapImageRep(forImage: image)

    guard let data = rep.representation(using: type, properties:[.compressionFactor: 1.0]) else {
    preconditionFailure()
    }

    try data.write(to: url)
}

...However, since I am working with a Mac App that is Sandboxed, which as you know is a requirement for Apple App Store distribution, I noticed that I had to choose my destination directory with care as I was testing my preliminary code.

If I used a file URL by way of:

let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentPath = documentsURL.path
let filePath = documentsURL.appendingPathComponent("TestImage.png")

filePath = file:///Users/Andrew/Library/Containers/Objects-and-Such.ColourSpace/Data/Documents/TestImage.png

...which works for sandboxed Apps, file saving won't work if I had chosen a directory such as Desktop:

filePath = file:///Users/Andrew/Library/Containers/Objects-and-Such.ColourSpace/Data/Desktop/TestImage.png

I hope this helps.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!