如何从 NSImage 保存 PNG 文件(视网膜问题)

问题描述 投票:0回答:6

我正在对图像进行一些操作,完成后,我想将图像以PNG格式保存在磁盘上。我正在做以下事情:

+ (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];
}

此代码可以正常工作,但在配备 Retina 屏幕的 Mac 上存在问题;如果我打印

NSBitmapImageRep
对象,我会得到不同大小和像素的矩形,当我的图像保存在磁盘上时,它的大小是原来的两倍:

$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>

我尝试强制像素大小不关心视网膜比例,因为我想保留原始大小:

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

这次,当我打印

NSBitmapImageRep
对象时,我得到了正确的尺寸,但是当我保存文件时,我仍然遇到同样的问题:

$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>

知道如何解决这个问题并保留原始像素大小吗?

macos retina-display image-resizing nsimage nsbitmapimagerep
6个回答
52
投票

如果您有

NSImage
并希望将其作为图像文件保存到文件系统,您应该 永远 使用
lockFocus
lockFocus
创建一个新图像,该图像被确定为在屏幕上显示,而不是其他任何内容。因此
lockFocus
使用屏幕的属性:normal 屏幕为 72 dpi,retina 屏幕为 144 dpi。对于你想要的,我建议使用以下代码:

+ (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];
}

16
投票
当您在配备视网膜屏幕的系统上

NSImage

 时,
lockFocus
具有分辨率感知功能并使用 HiDPI 图形上下文。
您传递给
NSBitmapImageRep
初始值设定项的图像尺寸以点(而不是像素)为单位。因此,150.0 点宽的图像在 @2x 上下文中使用 300 个水平像素。

您可以使用 convertRectToBacking:backingScaleFactor: 来补偿 @2x 上下文。 (我没有尝试),或者您可以使用以下

NSImage
类别,它创建具有明确像素尺寸的绘图上下文:

@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

6
投票

我在网上找到了这段代码,它可以在视网膜上运行。粘贴在这里,希望可以帮助别人。

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
投票

这是基于 Heinrich Giesen 的答案的 Swift 5 版本

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)")
    }
}

4
投票

以防万一有人偶然发现这个线程,这里有一个肯定有缺陷的解决方案,无论 Swift 中的设备如何,它都能以 1x 大小 (image.size) 保存图像:

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
    }
}

0
投票

我的 OS X 2 美分,包括处理扩展的写入 + 离屏图像绘制(方法 2);可以使用 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)
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.