Screenshot using Cocoa

Hello,

I am moving my code from GLUT to Cocoa (where I am a newbie), but I really like that GLUT on OSX allows you to copy a screenshot to the pasteboard just hitting cmd-c. The code below does that in a method belonging to a subclass of NSOpenGLView. It is largely a patchwork of ideas, which I found online.

Q: Could this have been achieved simpler? In particular, without resorting to a low level for loop and perhaps just cloning the first image rather than constructing a new bitmap image? Spawning a new process, I could probably do it with just a few lines, but that would probably be a rather heavy operation.

Thanks
Andreas


-(IBAction)save_window_to_pasteboard:(id)sender
{
    
    NSPasteboard *pb = [NSPasteboard generalPasteboard];
    
    // Telling the pasteboard what type of data we're going to send in
    [pb declareTypes:[NSArray arrayWithObjects:NSPasteboardTypePNG,nil] owner:self];
    
    NSRect backRect = [self convertRectToBacking: [self bounds]];
    NSSize sz;
    sz.width = NSWidth(backRect);
    sz.height = NSHeight(backRect);
    
    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: NULL
                                                                    pixelsWide: sz.width
                                                                    pixelsHigh: sz.height
                                                                 bitsPerSample: 8
                                                               samplesPerPixel: 3
                                                                      hasAlpha: NO
                                                                      isPlanar: NO
                                                                colorSpaceName: NSCalibratedRGBColorSpace
                                                                   bytesPerRow: 0                // indicates no empty bytes at row end
                                                                  bitsPerPixel: 0];
    
    glReadBuffer(GL_FRONT);
    int bytesPerRow = [rep bytesPerRow];
    glPixelStorei(GL_PACK_ROW_LENGTH, 8*bytesPerRow/[rep bitsPerPixel]);
    glReadPixels(0, 0, NSWidth(backRect), NSHeight(backRect), GL_RGB, GL_UNSIGNED_BYTE,  [rep bitmapData]);
    
    NSBitmapImageRep* flipped =  [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: NULL
                                                                         pixelsWide: sz.width
                                                                         pixelsHigh: sz.height
                                                                      bitsPerSample: 8
                                                                    samplesPerPixel: 3
                                                                           hasAlpha: NO
                                                                           isPlanar: NO
                                                                     colorSpaceName: NSCalibratedRGBColorSpace
                                                                        bytesPerRow: 0                // indicates no empty bytes at row end
                                                                       bitsPerPixel: 0];
    
    for(int j=0; j< sz.height; ++j)
        for(int i=0;i<sz.width;++i)
        {
            NSUInteger pixels[4];
            [rep getPixel: pixels atX:i y:j];
            [flipped setPixel: pixels atX:i y:sz.height-j];
        }
    
    // Converting the representation to PNG and sending it to the pasteboard (with type indicated)
    [pb setData:[flipped representationUsingType:NSPNGFileType properties:nil] forType:NSPasteboardTypePNG];
    
    
    
}

Hi Mac OpenGL Forum,

Hmm the code for snapping a screenshot seems to work and nobody has posted something better, so I am sticking with that. however, there were a few issues, so I am posting a new version. The main problem was the good old one-off issue: I was not writing to the top row of the image. That has been fixed. Also, I am now telling both Cocoa and OpenGL that the image should be densely packed in memory. Previously it was padded. Maybe that has slight performance gains, but this code is more clear, I think.

/Andreas

PS: Looking at the screenshot in Preview, I find that it is ever so slightly less saturated. That is probably something that preview does but curious all the same.


    // Get the pasteboard.
    NSPasteboard *pb = [NSPasteboard generalPasteboard];
    
    // Telling the pasteboard what type of data we're going to send in
    [pb declareTypes:[NSArray arrayWithObjects:NSPasteboardTypePNG,nil] owner:self];
    
    // Get the size of the image in a retina safe way
    NSRect backRect = [self convertRectToBacking: [self bounds]];
    int W = NSWidth(backRect);
    int H = NSHeight(backRect);


    // Create image. Note no alpha channel. I don't copy that.
    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: NULL
                                                                    pixelsWide: W
                                                                    pixelsHigh: H
                                                                 bitsPerSample: 8
                                                               samplesPerPixel: 3
                                                                      hasAlpha: NO
                                                                      isPlanar: NO
                                                                colorSpaceName: NSCalibratedRGBColorSpace
                                                                   bytesPerRow: 3*W
                                                                  bitsPerPixel: 0];
    
    // The following block does the actual reading of the image
    glPushAttrib(GL_PIXEL_MODE_BIT); // Save state about reading buffers
    glReadBuffer(GL_FRONT);
    glPixelStorei(GL_PACK_ALIGNMENT, 1); // Dense packing
    glReadPixels(0, 0, W, H, GL_RGB, GL_UNSIGNED_BYTE, [rep bitmapData]);
    glPopAttrib();
    
    // So we need one more image, since we must flip its orientation.
    NSBitmapImageRep* flipped =  [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: NULL
                                                                         pixelsWide: W
                                                                         pixelsHigh: H
                                                                      bitsPerSample: 8
                                                                    samplesPerPixel: 3
                                                                           hasAlpha: NO
                                                                           isPlanar: NO
                                                                     colorSpaceName: NSCalibratedRGBColorSpace
                                                                        bytesPerRow: 3*W
                                                                       bitsPerPixel: 0];
    
    // Primitive double for loop flipping the row order. Should be a better way. Can't find it.
    for(int j=1; j< H+1; ++j)
        for(int i=0;i<W;++i)
        {
            NSUInteger pixels[4];
            [rep getPixel: pixels atX:i y:j];
            [flipped setPixel: pixels atX:i y:H-j];
        }
    
    // Converting the representation to PNG and sending it to the pasteboard (with type indicated)
    [pb setData:[flipped representationUsingType:NSPNGFileType properties:nil] forType:NSPasteboardTypePNG];



Thanks for your code. It works a treat. I did find two ways to get this to work without the for loops, presumably using vector transforms in the CPU or GPU. One way is to use an NSImage with a “setFlipped” property, but it appears this has been deprecated since OSX 10.6. The other option is to use Core Image. I show this below. Here the user can specify to either write to a PNG image or to the clipboard.


- (void)saveScreenshotFromFileName:(NSString *) file_name //save PNG screenshot
{
    // Get the size of the image in a retina safe way
    NSRect backRect = [self convertRectToBacking: [self bounds]];
    int W = NSWidth(backRect);
    int H = NSHeight(backRect);
    // Create image. Note no alpha channel. I don't copy that.
    NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: NULL
        pixelsWide: W pixelsHigh: H bitsPerSample: 8 samplesPerPixel: 3 hasAlpha: NO
        isPlanar: NO colorSpaceName: NSCalibratedRGBColorSpace bytesPerRow: 3*W bitsPerPixel: 0];
    // The following block does the actual reading of the image
    glPushAttrib(GL_PIXEL_MODE_BIT); // Save state about reading buffers
    glReadBuffer(GL_FRONT);
    glPixelStorei(GL_PACK_ALIGNMENT, 1); // Dense packing
    glReadPixels(0, 0, W, H, GL_RGB, GL_UNSIGNED_BYTE, [rep bitmapData]);
    glPopAttrib();
    CIImage* ciimag = [[CIImage alloc] initWithBitmapImageRep: rep];
    CGAffineTransform trans = CGAffineTransformIdentity;
    trans = CGAffineTransformMakeTranslation(0.0f, H);
    trans = CGAffineTransformScale(trans, 1.0, -1.0);
    ciimag = [ciimag imageByApplyingTransform:trans];
    rep = [[NSBitmapImageRep alloc] initWithCIImage: ciimag];
    if ([file_name length] < 1) { //save to clipboard
        NSImage *imag = [[[NSImage alloc] init] autorelease];
        [imag addRepresentation:rep];
        NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
        [pasteboard clearContents];
        NSArray *copiedObjects = [NSArray arrayWithObject:imag];
        [pasteboard writeObjects:copiedObjects];
        return;
    }
    NSData *data = [rep representationUsingType: NSPNGFileType properties: nil];
    [data writeToFile: file_name atomically: NO];
}

This topic was automatically closed 183 days after the last reply. New replies are no longer allowed.