【问题标题】:How to save PNG file from NSImage (retina issues)如何从 NSImage 保存 PNG 文件(视网膜问题)
【发布时间】:2013-07-04 15:16:15
【问题描述】:

我正在对图像进行一些操作,完成后,我想将图像以 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];
}

这段代码可以工作,但问题出在视网膜 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


    【解决方案1】:

    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
    

    【讨论】:

    • 我尝试了一堆不同的解决方案(包括此处接受的答案)。这是我尝试过的唯一可行的解​​决方案。谢谢!
    • 我正在实施您的解决方案。我收到警告“isFlipped 已弃用:首先在 OS X 10.6 中弃用”。我应该忽略警告,还是更好地删除呼叫?
    • 另外,"Implicit conversion from enumeration type 'enum CGImageAlphaInfo' to different enumeration type 'CGBitmapInfo' (aka 'enum CGBitmapInfo')"。这似乎是一个更严重的警告。我检查了这两个枚举的定义,它们完全不同。但是,CGBitmapInfo 没有任何“预乘 alpha”常量。
    • 感谢您指出警告。我修正了我原来的答案。
    【解决方案2】:

    这是基于 Heinrich Giesen's answer 的 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)")
        }
    }
    

    【讨论】:

      【解决方案3】:

      我在 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)
              }
          }
      }
      

      【讨论】:

        【解决方案4】:

        以防有人偶然发现此线程。这肯定是有缺陷的解决方案,它可以将图像保存为 1x 大小(image.size),而不管 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
            }
        }
        

        【讨论】:

          【解决方案5】:

          我在 web 上找到了这段代码,它适用于视网膜。贴在这里,希望对大家有帮助。

          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]; 
          

          【讨论】:

          【解决方案6】:

          如果您有一个NSImage 并希望将其作为图像文件保存到文件系统,您应该永远不要使用lockFocuslockFocus 创建一个新图像,该图像被确定用于显示在屏幕上,仅此而已。因此lockFocus 使用屏幕的属性:普通 屏幕为 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];
          }
          

          【讨论】:

          • -[NSBitmapImageRep setSize:] 似乎仅从 10.10 开始可用。也许这就是为什么当我在 Mavericks 上尝试你的代码时,图像没有调整大小?虽然没有抛出异常......我得到的图像与原始尺寸相同,无论我通过什么尺寸。
          • @NicolasMiari 我确实看到newRep 的大小更改为应有的大小(目标为 10.9,但在 10.10 上运行),但写入磁盘的文件仍包含 2x 图像。你有没有想过解决方案?
          • @NicolasMiari 它也适用于我,但我需要一个中间的 NSData,它不会产生。所以我把它写到一个临时文件然后读进去。虽然它不是生产代码,只是一个单元测试。对我来说最重要的是它为视网膜和非视网膜屏幕(每像素相同)产生相同的输出,但它看起来不像。不同颜色之间的边界存在细微差别...
          • -[NSBitmapImageRep setSize:] 继承自 NSImageRep,从 10.0 开始可用
          • 不是来自点到像素对话的大小差异。如果你的 mac 屏幕是高清的,点使用的像素是两倍,所以,当你保存它时,它的大小是像素的两倍。屏幕上的绘图是点。
          猜你喜欢
          • 2014-06-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-03-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多