How to save screenshot of AGSMapView

4106
14
Jump to solution
02-20-2013 09:59 PM
HumzaAkhtar
Occasional Contributor II
Hi,

I am developing an iOS app using ArcGIS api and I wanted to put a feature of saving screenshot of the map. However using the normal rendering procedure results in a blank image. I am using the self.mapview.layers to access the layer before I save the current context as an image.  Below are some lines of code from my app which does the function of saving screenshots.


     UIGraphicsBeginImageContextWithOptions(self.mapview.frame.size,NO, 0.0);
[self.mapview.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(screenshot,self,@selector(image:didFinishSavingWithError:contextInfo:),nil);

There is another thread here which marks this code as the final answer but in the latest api 10.1, this just results in a blank image which is quite confusing for me.

I would appreciate if some one can help me deal with this.

thanks and Regards
H Akhtar
0 Kudos
1 Solution

Accepted Solutions
DanaMaher
Occasional Contributor
Hi,

...

There is another thread here which marks this code as the final answer but in the latest api 10.1, this just results in a blank image which is quite confusing for me.

I would appreciate if some one can help me deal with this.

thanks and Regards
H Akhtar


I provided the answer in the thread you reference and have recently worked around the issue you are now running into, which is actually an iOS 6.x issue, not strictly an ArcGIS SDK for iOS 10.1.1 issue. The fix is to set the kEAGLDrawablePropertyRetainedBacking property of your AGSMapView's CAEAGLLayer to YES prior to capturing an image and prior to showing any UIPopovercontrollers or other UI components.

The following code will set the kEAGLDrawablePropertyRetainedBacking property:
[INDENT]
    CAEAGLLayer *eaglLayer = (CAEAGLLayer *) theMap.layer;     eaglLayer.drawableProperties = @{                                      kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:YES],                                      kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8                                      };
[/INDENT]

And this block of code will snapshot the map's CAEAGLLayer:
[INDENT]
- (NSData *)getImage{          //the rest     GLint width;          GLint height;          glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);          glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);              NSInteger myDataLength = width * height * 4;          // allocate array and read pixels into it.     GLubyte *buffer = (GLubyte *) malloc(myDataLength);     glReadPixels(0, 0, width, height, 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 < height; y++)     {         for(int x = 0; x < width * 4; x++)         {             buffer2[((height - 1) - y) * width * 4 + x] = buffer[y * 4 * width + 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 * width;     CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();     CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;     CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;          // make the cgimage     CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);     //CGImageRef imageRefCopy = CGImageCreateCopy(imageRef);          // then make the uiimage from that     UIImage *myImage = [[UIImage alloc] initWithCGImage:imageRef];      NSData *iDat = UIImageJPEGRepresentation(myImage, .5);     CGImageRelease(imageRef);      CGDataProviderRelease(provider);     CGColorSpaceRelease(colorSpaceRef);     free(buffer);       free(buffer2);     return iDat;     }
[/INDENT]

I am returning an NSData object in the snapshot method, and not the generated UIImage, because the initWithCGImage/imageWithCGImage: methods of UIImage do not retain or transcribe the data of the CGImageRef that is passed in. If you follow the necessary memory management and release/free your core graphics objects, you will later get a hard crash when the UIImage tries to reference the released data. NSData *does* copy the CGImageRef's contents, avoiding this issue. Also, I needed to downsize the screenshot for sending it over the network.

This method can probably be refactored significantly, but it works and I have not had time to revisit it yet. My solution was adapted from this Stack Overflow thread. The iOS OpenGL Programming Guide explains the costs of setting kEAGLDrawablePropertyRetainedBacking to YES, and why you should return the flag to its default NO setting when you no longer need it. Again, you should set this flag before displaying any UIPopoverControllers or similar system controllers over the AGSMapView. If you set the flag after displaying a popover or modal controller, you will still get the blank screen.

View solution in original post

0 Kudos
14 Replies
DanaMaher
Occasional Contributor
Hi,

...

There is another thread here which marks this code as the final answer but in the latest api 10.1, this just results in a blank image which is quite confusing for me.

I would appreciate if some one can help me deal with this.

thanks and Regards
H Akhtar


I provided the answer in the thread you reference and have recently worked around the issue you are now running into, which is actually an iOS 6.x issue, not strictly an ArcGIS SDK for iOS 10.1.1 issue. The fix is to set the kEAGLDrawablePropertyRetainedBacking property of your AGSMapView's CAEAGLLayer to YES prior to capturing an image and prior to showing any UIPopovercontrollers or other UI components.

The following code will set the kEAGLDrawablePropertyRetainedBacking property:
[INDENT]
    CAEAGLLayer *eaglLayer = (CAEAGLLayer *) theMap.layer;     eaglLayer.drawableProperties = @{                                      kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:YES],                                      kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8                                      };
[/INDENT]

And this block of code will snapshot the map's CAEAGLLayer:
[INDENT]
- (NSData *)getImage{          //the rest     GLint width;          GLint height;          glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);          glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);              NSInteger myDataLength = width * height * 4;          // allocate array and read pixels into it.     GLubyte *buffer = (GLubyte *) malloc(myDataLength);     glReadPixels(0, 0, width, height, 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 < height; y++)     {         for(int x = 0; x < width * 4; x++)         {             buffer2[((height - 1) - y) * width * 4 + x] = buffer[y * 4 * width + 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 * width;     CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();     CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;     CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;          // make the cgimage     CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);     //CGImageRef imageRefCopy = CGImageCreateCopy(imageRef);          // then make the uiimage from that     UIImage *myImage = [[UIImage alloc] initWithCGImage:imageRef];      NSData *iDat = UIImageJPEGRepresentation(myImage, .5);     CGImageRelease(imageRef);      CGDataProviderRelease(provider);     CGColorSpaceRelease(colorSpaceRef);     free(buffer);       free(buffer2);     return iDat;     }
[/INDENT]

I am returning an NSData object in the snapshot method, and not the generated UIImage, because the initWithCGImage/imageWithCGImage: methods of UIImage do not retain or transcribe the data of the CGImageRef that is passed in. If you follow the necessary memory management and release/free your core graphics objects, you will later get a hard crash when the UIImage tries to reference the released data. NSData *does* copy the CGImageRef's contents, avoiding this issue. Also, I needed to downsize the screenshot for sending it over the network.

This method can probably be refactored significantly, but it works and I have not had time to revisit it yet. My solution was adapted from this Stack Overflow thread. The iOS OpenGL Programming Guide explains the costs of setting kEAGLDrawablePropertyRetainedBacking to YES, and why you should return the flag to its default NO setting when you no longer need it. Again, you should set this flag before displaying any UIPopoverControllers or similar system controllers over the AGSMapView. If you set the flag after displaying a popover or modal controller, you will still get the blank screen.
0 Kudos
HumzaAkhtar
Occasional Contributor II
Thanks a lot for this... However this only works in the simulator but the same code returns a blank screen in the device.

Any idea why this might be.\

Thanks again
0 Kudos
DanaMaher
Occasional Contributor
Thanks a lot for this... However this only works in the simulator but the same code returns a blank screen in the device.

Any idea why this might be.\

Thanks again


The code I pasted above is working on the device, for me. Possibly, you need to be setting the render flags somewhere else. As an experiment, try setting the render flags when your AGSMapView is first loaded, in the  -(void)viewDidLoad method of the UIViewController containing the map view. This should cause the map to render correctly on the device. Then, start moving the render flags forward in your code, towards the method/interaction that triggers the map export, until they stop working. As I noted previously, I had to experiment with the placement of the render flags; setting the flags after you have displayed a UIPopover, UIAlertView or similar dialogue will cause the map render to fail.

If you can post your code, that would help with troubleshooting immensely.
0 Kudos
OguzhanTopsakal
New Contributor III
Thanks a lot for this... However this only works in the simulator but the same code returns a blank screen in the device.

Any idea why this might be.\

Thanks again


Have you solved this problem? I am facing the same issue (not working on the device) and moving the following code did not help as suggested:
    CAEAGLLayer *eaglLayer = (CAEAGLLayer *) theMap.layer;
    eaglLayer.drawableProperties = @{
                                     kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:YES],
                                     kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
                                     };


I would appreciate if you could share your solution.

Thanks.
0 Kudos
JeffGiesler
Occasional Contributor
Hey Guys,
I have followed the above example and tried moving the renderers around.  Like some of the other comments the code works great in the simulator but when I put it onto my iPad, I get a black image.  Has anyone figured this out?  I am sure I am just doing something wrong.
Cheers,
Jeff
0 Kudos
OguzhanTopsakal
New Contributor III
Hey Guys,
I have followed the above example and tried moving the renderers around.  Like some of the other comments the code works great in the simulator but when I put it onto my iPad, I get a black image.  Has anyone figured this out?  I am sure I am just doing something wrong.
Cheers,
Jeff


On the device, try sending the app to the background and bringing it back to the foreground. For some reason, it starts working after bringing the app back from the background..

I am looking forward to get an answer/comment from ArcGIS team on this issue.
0 Kudos
JeffGiesler
Occasional Contributor
I have to say that is probably the quickest response I have ever gotten. Unfortunately that didn't work for me, even just as a temp work around.  Any other ideas / work arounds.
Cheers,
Jeff
0 Kudos
HumzaAkhtar
Occasional Contributor II
i believe that your issue is in the settings of flags. Try adding this code in the method didopenwebmap

CAEAGLLayer *eaglLayer = (CAEAGLLayer *) self.mapView.layer;
   
    eaglLayer.drawableProperties = @{
                                     kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:YES],
                                     kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
                                     };
0 Kudos
JeffGiesler
Occasional Contributor
Humza,
I have tried that but maybe I am putting it in the wrong spot or doing something else wrong.  I am attaching my code for both calling the get image process and the process itself.  Any help would be greatly appreciated. 
Calling
-(void) PrintPermitFinal
{
CAEAGLLayer *eaglLayer = (CAEAGLLayer *) self.mapView.layer;
    eaglLayer.drawableProperties = @{
                                     kEAGLDrawablePropertyRetainedBacking: [NSNumber numberWithBool:YES],
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
                                     };
// reset buttons
    [selfsettoInitialMapView];
// create image and pdf
    UIImage *screenshot = [[UIImage alloc] initWithData:[self getImage]];
// use clip path Rect to clip image to right size
CGImageRef imageref = CGImageCreateWithImageInRect([screenshot CGImage], clipRect);
    screenshot = [UIImage imageWithCGImage:imageref];
//save image to photo album for testing only
    UIImageWriteToSavedPhotosAlbum(screenshot,self,@selector(image:didFinishSavingWithError:contextInfo:),nil);
//Send Image to Dictionary
if (AppDelagate.PermitImages == nil)
    {
AppDelagate.PermitImages = [[NSMutableDictionaryalloc] init];
    }
NSString *Key= [selfCreatePermitID:[AppDelagate.GraphicToPrint.allAttributesvalueForKey:@"Full_Name"] AndAddress:[AppDelagate.GraphicToPrint.allAttributesvalueForKey:@"Owner_Address"]];
    [AppDelagate.PermitImages setObject:screenshot forKey:Key];
self.tabBarController.selectedViewController = [self.tabBarController.viewControllersobjectAtIndex:1];
}

Getting the image virtually unchanged from Bernese
- (NSData *)getImage
{
    GLint width;

    GLint height;

glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);

glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);

    NSInteger myDataLength = width * height * 4;

// allocate array and read pixels into it.
    GLubyte *buffer = (GLubyte *) malloc(myDataLength);
    glReadPixels(0, 0, width, height, 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 < height; y++)
    {
        for(int x = 0; x < width * 4; x++)
        {
            buffer2[((height - 1) - y) * width * 4 + x] = buffer[y * 4 * width + 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 * width;
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;

// make the cgimage
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

// then make the uiimage from that
    UIImage *myImage = [[UIImage alloc] initWithCGImage:imageRef];
    NSData *iDat = UIImageJPEGRepresentation(myImage, 0);
    CGImageRelease(imageRef);
CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpaceRef);
    free(buffer);
    free(buffer2);
    return iDat;
}

Cheers,
Jeff
0 Kudos