Как сохранить PNG файл из NSImage (проблемы сетчатки)

Я делаю некоторые операции над изображениями, и после того, как я закончил, я хочу сохранить изображение как 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>

Любая идея, как исправить это, и сохранить исходный размер пикселя?

Ответ 1

Если у вас есть NSImage и вы хотите сохранить его как файл изображения в файловой системе, вы должны никогда использовать lockFocus! lockFocus создает новое изображение, которое определяется для отображения экрана и ничего другого. Поэтому lockFocus использует свойства экрана: 72 dpi для обычных экранов и 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];
}

Ответ 2

NSImage имеет разрешение и использует графический контекст HiDPI, когда вы lockFocus в системе со сеткой сетчатки.
Размеры изображения, которые вы передаете на ваш инициализатор NSBitmapImageRep, находятся в точках (а не в пикселях). Таким образом, изображение шириной 150,0 точек использует 300 горизонтальных пикселей в контексте @2x.

Вы можете использовать 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

Ответ 3

Я нашел этот код в Интернете, и он работает на сетчатке. Вставьте сюда, надеюсь, кто-то может помочь.

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

Ответ 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

Мои 2 цента для OS X, включая запись, обрабатывают расширения + экранный рисунок изображения (метод 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)
        }
    }
}

Ответ 6

Вот версия 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)")
    }
}