Как сохранить файл 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>

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


person Ludovic Landry    schedule 06.07.2013    source источник


Ответы (6)


Если у вас есть NSImage и вы хотите сохранить его как файл изображения в файловой системе, вам не следует никогда использовать lockFocus! lockFocus создает новое изображение, предназначенное для показа на экране и ни для чего другого. Поэтому lockFocus использует свойства экрана: 72 dpi для обычных экранов и 144 dpi для экранов retina. Для того, что вы хотите, я предлагаю следующий код:

+ (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];
}
person Heinrich Giesen    schedule 07.07.2013
comment
-[NSBitmapImageRep setSize:] кажется доступным только с 10.10. Возможно, поэтому, когда я попробовал ваш код на Mavericks, размер изображения не изменился? Хотя никаких исключений не выдается... Я получаю изображение того же размера, что и оригинал, независимо от того, какой размер я передаю. - person Nicolas Miari; 12.12.2014
comment
@NicolasMiari Я вижу, что размер newRep меняется на то, каким он должен быть (нацелен на 10.9, но работает на 10.10), но файл, записанный на диск, по-прежнему содержит изображение 2x. Вы когда-нибудь придумывали решение? - person Dov; 30.12.2014
comment
@NicolasMiari У меня это тоже работает, но мне нужен промежуточный NSData, который он не создает. Поэтому я пишу это во временный файл, а затем читаю его. Это не производственный код, а просто модульный тест. Самое главное для меня то, что он выдает одинаковый результат для экранов с сетчаткой и без сетчатки (попиксельно идентичен), чего, похоже, не происходит. Есть небольшие различия на границах между разными цветами... - person Dov; 30.12.2014
comment
-[NSBitmapImageRep setSize:] унаследован от NSImageRep и доступен с версии 10.0. - person Heinrich Giesen; 15.01.2015
comment
Разве разница в размерах не возникает из точки в пиксельном разговоре. Если ваш экран Mac HD, точки используют в два раза больше пикселей, поэтому при сохранении они в два раза больше, чем пиксели. Рисунок на экране в виде точек. - person Hope; 13.07.2016

NSImage учитывает разрешение и использует графический контекст HiDPI, когда вы lockFocus работаете в системе с экраном Retina.
Размеры изображения, которые вы передаете инициализатору 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
person Thomas Zoechling    schedule 07.07.2013
comment
Я пробовал кучу разных решений (включая принятый ответ здесь). Это единственное решение, которое я пробовал, которое работает. Спасибо! - person EsbenB; 16.02.2014
comment
Я реализую ваше решение. Я получаю предупреждение isFlipped устарело: сначала устарело в OS X 10.6. Должен ли я игнорировать предупреждение или лучше удалить вызов? - person Nicolas Miari; 12.12.2014
comment
Кроме того, "Implicit conversion from enumeration type 'enum CGImageAlphaInfo' to different enumeration type 'CGBitmapInfo' (aka 'enum CGBitmapInfo')". Это, кажется, более серьезное предупреждение. Я проверил определение этих двух перечислений, и они совершенно разные. Однако CGBitmapInfo не имеет константы для «предварительно умноженной альфы». - person Nicolas Miari; 12.12.2014
comment
Спасибо за указание на предупреждения. Я исправил свой первоначальный ответ. - person Thomas Zoechling; 12.12.2014

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

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]; 
person flowers    schedule 20.03.2015
comment
gist.github.com/mattstevens/4400775 Изменение размера NSImage на компьютерах Mac с сетчаткой для вывода в виде 1x изображение - person flowers; 20.03.2015
comment
Это не кажется прямым ответом на вопрос - person abarisone; 20.03.2015

Просто на всякий случай, если кто-нибудь наткнется на эту тему. Вот, безусловно, ошибочное решение, которое выполняет работу по сохранению изображения в 1-кратном размере (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
    }
}
person stringCode    schedule 19.08.2015

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

Мои 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)
        }
    }
}
person Marek H    schedule 13.09.2016