web-dev-qa-db-fra.com

Comment enregistrer un fichier PNG à partir de NSImage (problèmes de rétine)

Je fais quelques opérations sur les images et une fois terminé, je veux enregistrer l'image au format PNG sur le disque. Je fais ce qui suit:

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

Ce code fonctionne, mais le problème vient de retina mac. Si j'imprime l'objet NSBitmapImageRep, je reçois une taille et des pixels différents et, lorsque mon image est enregistrée sur le disque, sa taille est deux fois plus grande:

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

J'ai lié pour forcer la taille des pixels à ne pas prendre en compte l'échelle de la rétine, car je veux conserver la taille d'origine:

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

Cette fois, je reçois la bonne taille lorsque j'imprime l'objet NSBitmapImageRep, mais lorsque je sauvegarde mon fichier, le problème persiste:

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

Avez-vous une idée de comment résoudre ce problème et préserver la taille de pixel d'origine?

24
Ludovic Landry

Si vous avez une NSImage et que vous voulez l'enregistrer en tant que fichier image dans le système de fichiers, vous devriez jamais utiliser lockFocus! lockFocus crée une nouvelle image qui est déterminée pour être affichée à l'écran et rien d'autre. Par conséquent, lockFocus utilise les propriétés de l'écran: 72 ppp pour les écrans normaux et 144 ppp pour les écrans retina. Pour ce que vous voulez, je propose le code suivant:

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

NSImage est sensible à la résolution et utilise un contexte graphique HiDPI lorsque vous lockFocus sur un système avec écran Retina.
Les dimensions de l'image que vous transmettez à votre initialiseur NSBitmapImageRep sont en points (et non en pixels). Une image de 150,0 points utilise par conséquent 300 pixels horizontaux dans un contexte @ 2x. 

Vous pouvez utiliser convertRectToBacking: ou backingScaleFactor: pour compenser le contexte @ 2x. (Je n'ai pas essayé cela), ou vous pouvez utiliser la catégorie NSImage suivante, qui crée un contexte de dessin avec des dimensions de pixels explicites:

@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
14
Thomas Zoechling

J'ai trouvé ce code sur le Web, et cela fonctionne sur la rétine. Collez ici, l'espoir peut aider quelqu'un.

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
flowers

Juste au cas où quelqu'un trébuche sur ce fil. Voici une solution imparfaite qui permet d’enregistrer une image au format 1x (taille d'image), quel que soit le périphérique utilisé

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

Mes 2 cents pour OS X, y compris l’écriture qui gère les extensions + le dessin d’image hors écran (méthode 2); on peut vérifier avec 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)
        }
    }
}
0
Marek H