14

I am trying to get a UIImage from what is displayed in my EAGLView. Any suggestions on how to do this?

RexOnRoids
  • 14,002
  • 33
  • 96
  • 136

6 Answers6

10

Here is a cleaned up version of Quakeboy's code. I tested it on iPad, and works just fine. The improvements include:

  • works with any size EAGLView
  • works with retina display (point scale 2)
  • replaced nested loop with memcpy
  • cleaned up memory leaks
  • saves the UIImage in the photoalbum as a bonus.

Use this as a method in your EAGLView:

-(void)snapUIImage
{
    int s = 1;
    UIScreen* screen = [ UIScreen mainScreen ];
    if ( [ screen respondsToSelector:@selector(scale) ] )
        s = (int) [ screen scale ];

    const int w = self.frame.size.width;
    const int h = self.frame.size.height;
    const NSInteger myDataLength = w * h * 4 * s * s;
    // allocate array and read pixels into it.
    GLubyte *buffer = (GLubyte *) malloc(myDataLength);
    glReadPixels(0, 0, w*s, h*s, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    // gl renders "upside down" so swap top to bottom into new array.
    // there's gotta be a better way, but this works.
    GLubyte *buffer2 = (GLubyte *) malloc(myDataLength);
    for(int y = 0; y < h*s; y++)
    {
        memcpy( buffer2 + (h*s - 1 - y) * w * 4 * s, buffer + (y * 4 * w * s), w * 4 * s );
    }
    free(buffer); // work with the flipped buffer, so get rid of the original one.

    // make data provider with data.
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer2, myDataLength, NULL);
    // prep the ingredients
    int bitsPerComponent = 8;
    int bitsPerPixel = 32;
    int bytesPerRow = 4 * w * s;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    // make the cgimage
    CGImageRef imageRef = CGImageCreate(w*s, h*s, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    // then make the uiimage from that
    UIImage *myImage = [ UIImage imageWithCGImage:imageRef scale:s orientation:UIImageOrientationUp ];
    UIImageWriteToSavedPhotosAlbum( myImage, nil, nil, nil );
    CGImageRelease( imageRef );
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpaceRef);
    free(buffer2);
}
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
Bram
  • 7,440
  • 3
  • 52
  • 94
8

I was unable to get the other answers here to work correctly for me.

After a few days I finally got a working solution to this. There is code provided by Apple which produces a UIImage from a EAGLView. Then you simply need to flip the image vertically since UIkit is upside down.

Apple Provided Method - Modified to be inside the view you want to make into an image.

    -(UIImage *) drawableToCGImage 
{
GLint backingWidth2, backingHeight2;
//Bind the color renderbuffer used to render the OpenGL ES view
// If your application only creates a single color renderbuffer which is already bound at this point,
// this call is redundant, but it is needed if you're dealing with multiple renderbuffers.
// Note, replace "_colorRenderbuffer" with the actual name of the renderbuffer object defined in your class.
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);

// Get the size of the backing CAEAGLLayer
glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &backingWidth2);
glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &backingHeight2);

NSInteger x = 0, y = 0, width2 = backingWidth2, height2 = backingHeight2;
NSInteger dataLength = width2 * height2 * 4;
GLubyte *data = (GLubyte*)malloc(dataLength * sizeof(GLubyte));

// Read pixel data from the framebuffer
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(x, y, width2, height2, GL_RGBA, GL_UNSIGNED_BYTE, data);

// Create a CGImage with the pixel data
// If your OpenGL ES content is opaque, use kCGImageAlphaNoneSkipLast to ignore the alpha channel
// otherwise, use kCGImageAlphaPremultipliedLast
CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, dataLength, NULL);
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGImageRef iref = CGImageCreate(width2, height2, 8, 32, width2 * 4, colorspace, kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast,
                                ref, NULL, true, kCGRenderingIntentDefault);

// OpenGL ES measures data in PIXELS
// Create a graphics context with the target size measured in POINTS
NSInteger widthInPoints, heightInPoints;
if (NULL != UIGraphicsBeginImageContextWithOptions) {
    // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to take the scale into consideration
    // Set the scale parameter to your OpenGL ES view's contentScaleFactor
    // so that you get a high-resolution snapshot when its value is greater than 1.0
    CGFloat scale = self.contentScaleFactor;
    widthInPoints = width2 / scale;
    heightInPoints = height2 / scale;
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(widthInPoints, heightInPoints), NO, scale);
}
else {
    // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
    widthInPoints = width2;
    heightInPoints = height2;
    UIGraphicsBeginImageContext(CGSizeMake(widthInPoints, heightInPoints));
}

CGContextRef cgcontext = UIGraphicsGetCurrentContext();

// UIKit coordinate system is upside down to GL/Quartz coordinate system
// Flip the CGImage by rendering it to the flipped bitmap context
// The size of the destination area is measured in POINTS
CGContextSetBlendMode(cgcontext, kCGBlendModeCopy);
CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, widthInPoints, heightInPoints), iref);

// Retrieve the UIImage from the current context
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

// Clean up
free(data);
CFRelease(ref);
CFRelease(colorspace);
CGImageRelease(iref);

return image;

}

And heres a method to flip the image

- (UIImage *) flipImageVertically:(UIImage *)originalImage {
UIImageView *tempImageView = [[UIImageView alloc] initWithImage:originalImage];
UIGraphicsBeginImageContext(tempImageView.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform flipVertical = CGAffineTransformMake(
                                                       1, 0, 0, -1, 0, tempImageView.frame.size.height
                                                       );
CGContextConcatCTM(context, flipVertical);

[tempImageView.layer renderInContext:context];

UIImage *flippedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//[tempImageView release];

return flippedImage;

}

And here's a link to the Apple dev page where I found the first method for reference. http://developer.apple.com/library/ios/#qa/qa1704/_index.html

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
Nate_Hirsch
  • 671
  • 1
  • 6
  • 12
  • Nate, I moved your answer from that question (which was a duplicate of this one) into here, so that people would have one go-to source for the various ways of capturing from an OpenGL ES scene. – Brad Larson Aug 08 '12 at 18:29
  • @Nate: Is there any way to know if there is any content scribbled on the EAGLView. I am also trying to take screenshot of the view which as described above worked but if user takes screenshot or clears the screen and takes the screenshot then empty screen is captured which i want to avoid. – Arun Gupta Mar 01 '16 at 17:07
6
-(UIImage *) saveImageFromGLView
{
    NSInteger myDataLength = 320 * 480 * 4;
    // allocate array and read pixels into it.
    GLubyte *buffer = (GLubyte *) malloc(myDataLength);
    glReadPixels(0, 0, 320, 480, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    // gl renders "upside down" so swap top to bottom into new array.
    // there's gotta be a better way, but this works.
    GLubyte *buffer2 = (GLubyte *) malloc(myDataLength);
    for(int y = 0; y <480; y++)
    {
        for(int x = 0; x <320 * 4; x++)
        {
            buffer2[(479 - y) * 320 * 4 + x] = buffer[y * 4 * 320 + x];
        }
    }
    // make data provider with data.
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer2, myDataLength, NULL);
    // prep the ingredients
    int bitsPerComponent = 8;
    int bitsPerPixel = 32;
    int bytesPerRow = 4 * 320;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    // make the cgimage
    CGImageRef imageRef = CGImageCreate(320, 480, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    // then make the uiimage from that
    UIImage *myImage = [UIImage imageWithCGImage:imageRef];

    CGImageRelease( imageRef );
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpaceRef);
    free(buffer2);

    return myImage;
}
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • The above code has a few leaks in it, so I'd highly recommend using the more efficient code provided in Bram's answer instead. – Brad Larson Nov 25 '11 at 21:27
  • 2
    I went ahead and incorporated the more critical memory leak fixes from Bram's code, just in case future developers copy and paste this code into their own applications. – Brad Larson Nov 25 '11 at 21:56
  • Thank you for fixing the leaks! – Rajavanya Subramaniyan Nov 30 '11 at 18:24
  • Hello guys, I don't know OpenGL and I don't understand, where in this code you indicate needed EAGLView ? – Igor Bidiniuc Dec 25 '12 at 01:48
  • I have GPUImageView and I can't get from it UIImage and I thought that simple way is to get image from it... How I can do this ? Please any suggestions. P.S. Brad, respect for framework (v) – Igor Bidiniuc Dec 25 '12 at 01:51
  • I'm having a problem with the free(buffer2) at the end which is causing a crash somewhere. – Christian J. B. May 25 '14 at 22:43
  • Using `[UIImage imageWithCGImage:imageRef]` causes `myImage` to depend on `buffer2`. That means `free(buffer2)` will cause memory corruption, since `myImage` is still needed later. I recommend drawing `imageRef` in a context, then getting the image from it. – Jeffrey Sun May 02 '15 at 20:25
  • Nope, this is the correct answer. I've used similar successfully. I've posted the code here: https://demonicactivity.blogspot.com/2016/11/tech-serious-ios-developers-use-every.html As to the memory issue, you'll see how I addressed that. Quite easy. – James Bush Nov 05 '16 at 06:42
1

EDIT: as demianturner notes below, you no longer need to render the layer, you can (and should) now use the higher-level [UIView drawViewHierarchyInRect:]. Other than that; this should work the same.

An EAGLView is just a kind of view, and its underlying CAEAGLLayer is just a kind of layer. That means, that the standard approach for converting a view/layer into a UIImage will work. (The fact that the linked question is UIWebview doesn't matter; that's just yet another kind of view.)

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 3
    Thanks for the answer Rob, but I tried the "Standard Method" and it did not work. Even though my EAGLView is correctly displaying a texture loaded to it, the only UIImage I have been able to extract from it using the STANDARD APPROACH is one with a blank white color, which is exactly how the EAGLView appears in IB. This is strange, indeed, seeing how EAGLView is just a kind of UIView. I think maybe I need to use glReadPixels or something instead? I am working with an EAGLView from the Apple Sample Code GLImageProcessing example. – RexOnRoids Aug 30 '09 at 03:31
  • Eh... I really did speak too fast. OpenGL doesn't render the same way as Quartz. Sorry about that. I believe this thread will address your problem. Read through all the messages; he works out several bugs along the way. I'm assuming you already know how to deal with a CGContext and get a CGImage out of it (and a UIImage from that). http://lists.apple.com/archives/mac-opengl/2006//jan/msg00085.html – Rob Napier Aug 30 '09 at 04:09
  • 1
    All these answers are out of date and unnecessarily long-winded. The solution only requires 4 lines of UIKit code, see https://developer.apple.com/library/content/qa/qa1817/_index.html – Demian Turner Apr 25 '17 at 16:42
  • @demianturner That looks exactly like the linked answer (under "the standard approach.") Is there something else you meant? – Rob Napier Apr 25 '17 at 18:49
  • Hi @RobNapier - yes my linked answer is different. This SA page wasted about 3 hours of my time. The linked answer (following link 'standard approach' which leads to http://stackoverflow.com/questions/858788/convert-uiwebview-contents-to-a-uiimage ) suggests you call UIView's `renderInContext` method. This is incorrect. What I linked to gives the right answer which is that the `drawViewHierarchyInRect` method is needed. All answers on this page, and I tried each one, sugges going down to OpenGL which is not necessary. – Demian Turner Apr 26 '17 at 08:29
0

CGDataProviderCreateWithData comes with a release callback to release the data, where you should do the release:

void releaseBufferData(void *info, const void *data, size_t size)
{
    free((void*)data);
}

Then do this like other examples, but NOT to free data here:

GLubyte *bufferData = (GLubyte *) malloc(bufferDataSize);
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, bufferData, bufferDataSize, releaseBufferData);
....
CGDataProviderRelease(provider);

Or simply use CGDataProviderCreateWithCFData without release callback stuff instead:

GLubyte *bufferData = (GLubyte *) malloc(bufferDataSize);
NSData *data = [NSData dataWithBytes:bufferData length:bufferDataSize];
CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data);
....
CGDataProviderRelease(provider);
free(bufferData); // Remember to free it

For more information, please check this discuss:

What's the right memory management pattern for buffer->CGImageRef->UIImage?

Community
  • 1
  • 1
Davis Cho
  • 340
  • 4
  • 12
0

With this above code of Brad Larson, you have to edit your EAGLView.m

- (id)initWithCoder:(NSCoder*)coder{
    self = [super initWithCoder:coder];
    if (self) {
        CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
        eaglLayer.opaque = TRUE;
        eaglLayer.drawableProperties = 
            [NSDictionary dictionaryWithObjectsAndKeys: 
                [NSNumber numberWithBool:YES],  kEAGLDrawablePropertyRetainedBacking, 
                kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
    }
    return self;
}

You have to change numberWithBool value YES

Jeffrey Sun
  • 7,789
  • 1
  • 24
  • 17
Mohit Tomar
  • 5,173
  • 2
  • 33
  • 40