Capturing the contents of a WebView

So one of the upcoming features for Two 1.1 is converting the contents of the mobile view into a PNG for later use. This seems like an easy task, but can be tricky if you don't think it through.

I thought I would share the process I am using for accomplishing this task in hopes of saving some poor soul the endless google'ing. Now this isn't the only way to accomplish this, hell I am pretty sure it isn't the best, but it does work and is pretty easy to grock.

As is my custom, here is the entire method to look at, before we break it down into smaller chunks. You can also check it out as a gist if you prefer.

- (IBAction)getImageFromWeb:(id)sender {
    // grab the width and height of the document in our mobileView.
    CGSize contentSize = CGSizeMake(
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth;"] floatValue],
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"] floatValue]
    );
// create a new window, offscreen.
NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, contentSize.width, contentSize.height  ) 
styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];

// grab the dimensions of the viewport of the mobileView, and create a frame for our WebView that matches the dimensions of the document that is loaded.
NSView *viewport = [[[mobileView mainFrame] frameView] documentView]; // width/height of html page
NSRect viewportBounds = [viewport bounds];
NSRect frame = NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height);

// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

// grab the value of textField as a string
NSString *hURL = [textField stringValue];

// set hiddenWebView as the content of hiddenWindow
[hiddenWindow setContentView:hiddenWebView];

// call loadRequest on hiddenWebView, converting our hURL string to a URLRequest object.
[[hiddenWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:hURL]]];

// we lock our focus on the WebView. This becomes important later when we want to capture this WebView.
[hiddenWebView lockFocus];

// as long as the URL is loading, stick right here.
while ([hiddenWebView isLoading]) {
    [hiddenWebView setNeedsDisplay:NO];
    [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate dateWithTimeIntervalSinceNow:1.0] inMode:NSDefaultRunLoopMode dequeue:YES];
}

// once the loading condition is satisfied we can move on.
[hiddenWebView setNeedsDisplay:YES];

// the first step is to create a bitmap (referenced internally as bitmap) that corresponds to the dimensions of our WebView.
// you can see now why we locked focus on the WebView, as we are creating this bitmap by telling it to look at the FocusedView.
bitmap = [[NSBitmapImageRep alloc] initWithFocusedViewRect:viewportBounds];

// now that we have grabbed our image, lets release the focus on the WebView.
[h iddenWebView unlockFocus];

// convert our bitmap to an image for previewing.
NSImage *dispImage = [[NSImage alloc] initWithData:[bitmap TIFFRepresentation]];

// Display the image by assigning it to an NSImageView.
[imagePreview setImage:dispImage];

}

Step one, take it offscreen

Because of the specific design of my app, it was necessary to take the show on the road, and do the image capture off screen. This simply means that we are going to programmatically create a new NSWindow and attach a newly created WebView to it. That can be done in a few lines of code:

- (IBAction)getImageFromWeb:(id)sender {
    // create a new window, offscreen.
    NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, 100, 100 ) 
    styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];
// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

}

Okay, so we create our new IBAction getImageFromWeb and throw in our creation syntax for our window and WebView. An IBAction is a method that responds to an interface element, in this case a button. The Method accepts one bit of info, which is a record that corresponds to the item that "sent" the call to action.

We will use that a little later in the game. For now, let's look at the syntax used to crate our NSWindow. First we tell Xcode the name of our new thingy, and what that thingy is. In this case we are creating a window and calling it hiddenWindow. We alloc the window (allocate) and pass it a Rect to use when initializing the window.

This rect contains screen position coordinates, width and height. As you can see we are passing negative values as the first two values of our rect. These are the x and y positioning coords. Passing high value negative numbers ensures that our new window will be drawn offscreen.

Lastly we pass a width and height to set the new window to. While 100 pixels by 100 pixels is nice and all, it doesn't really help us since we want to take a snap of the entire document that is in our mobile WebView. What we really need is a way to find the dimensions of the document that has been loaded into the view. Enter our friend stringByEvaluatingJavaScriptFromString.

- (IBAction)getImageFromWeb:(id)sender {
    // grab the width and height of the document in our mobileView.
    CGSize contentSize = CGSizeMake(
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth;"] floatValue],
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"] floatValue]
    );
// create a new window, offscreen.
NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, contentSize.width, contentSize.height  ) 
styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];

// grab the dimensions of the viewport of the mobileView, and create a frame for our WebView that matches the dimensions of the document that is loaded.
NSView *viewport = [[[mobileView mainFrame] frameView] documentView]; // width/height of html page
NSRect viewportBounds = [viewport bounds];
NSRect frame = NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height);

// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

}

Okay, so now we have the width and height of the document was loaded into the mobile WebView mobileView as it is referenced internally. We can now pass those values on to our window creation method, ensuring that the window will display the entire site, not just a portion.

Lastly we need to attach our newly created WebView to our window.

- (IBAction)getImageFromWeb:(id)sender {
    // grab the width and height of the document in our mobileView.
    CGSize contentSize = CGSizeMake(
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth;"] floatValue],
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"] floatValue]
    );
// create a new window, offscreen.
NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, contentSize.width, contentSize.height  ) 
styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];

// grab the dimensions of the viewport of the mobileView, and create a frame for our WebView that matches the dimensions of the document that is loaded.
NSView *viewport = [[[mobileView mainFrame] frameView] documentView]; // width/height of html page
NSRect viewportBounds = [viewport bounds];
NSRect frame = NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height);

// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

// set hiddenWebView as the content of hiddenWindow
[hiddenWindow setContentView:hiddenWebView];

}

Okay, so we now have a window created offscreen and a WebView attached to it. Let's look at how to programmatically load the current site we are testing in our hidden window.

Grabbing and assigning a URL

Two has a hidden URL field that is constantly updated with the current URL. It is internally referenced as textField... inventive I know, but there it is. So we need to grab the value from textField and pass it to hiddenWebView and tell it to load the URL. Pretty simple actually.

- (IBAction)getImageFromWeb:(id)sender {
    // grab the width and height of the document in our mobileView.
    CGSize contentSize = CGSizeMake(
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth;"] floatValue],
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"] floatValue]
    );
// create a new window, offscreen.
NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, contentSize.width, contentSize.height  ) 
styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];

// grab the dimensions of the viewport of the mobileView, and create a frame for our WebView that matches the dimensions of the document that is loaded.
NSView *viewport = [[[mobileView mainFrame] frameView] documentView]; // width/height of html page
NSRect viewportBounds = [viewport bounds];
NSRect frame = NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height);

// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

// grab the value of textField as a string
NSString *hURL = [textField stringValue];

// set hiddenWebView as the content of hiddenWindow
[hiddenWindow setContentView:hiddenWebView];

// call loadRequest on hiddenWebView, converting our hURL string to a URLRequest object.
[[hiddenWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:hURL]]];

// we lock our focus on the WebView. This becomes important later when we want to capture this WebView.
[hiddenWebView lockFocus];

}

Okay so far so good. We have created our window, attached a WebView and told it to load the URL we are currently looking at. All that is left to do is to grab the contents that have loaded and convert them to a PNG. right?

Yes, but first we have to be sure that the WebView has finished loading the requested URL.

- (IBAction)getImageFromWeb:(id)sender {
    // grab the width and height of the document in our mobileView.
    CGSize contentSize = CGSizeMake(
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth;"] floatValue],
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"] floatValue]
    );
// create a new window, offscreen.
NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, contentSize.width, contentSize.height  ) 
styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];

// grab the dimensions of the viewport of the mobileView, and create a frame for our WebView that matches the dimensions of the document that is loaded.
NSView *viewport = [[[mobileView mainFrame] frameView] documentView]; // width/height of html page
NSRect viewportBounds = [viewport bounds];
NSRect frame = NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height);

// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

// grab the value of textField as a string
NSString *hURL = [textField stringValue];

// set hiddenWebView as the content of hiddenWindow
[hiddenWindow setContentView:hiddenWebView];

// call loadRequest on hiddenWebView, converting our hURL string to a URLRequest object.
[[hiddenWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:hURL]]];

// we lock our focus on the WebView. This becomes important later when we want to capture this WebView.
[hiddenWebView lockFocus];

// as long as the URL is loading, stick right here.
while ([hiddenWebView isLoading]) {
    [hiddenWebView setNeedsDisplay:NO];
    [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate dateWithTimeIntervalSinceNow:1.0] inMode:NSDefaultRunLoopMode dequeue:YES];
}

// once the loading condition is satisfied we can move on.
[hiddenWebView setNeedsDisplay:YES];

}

Okay, we now have loaded the URL in our hidden WebView and made sure we didn't move on to capturing an image until the page is actually loaded. Now, on to the last step, taking that picture.

- (IBAction)getImageFromWeb:(id)sender {
    // grab the width and height of the document in our mobileView.
    CGSize contentSize = CGSizeMake(
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollWidth;"] floatValue],
        [[mobileView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"] floatValue]
    );
// create a new window, offscreen.
NSWindow *hiddenWindow = [[NSWindow alloc] initWithContentRect: NSMakeRect( -1000,-1000, contentSize.width, contentSize.height  ) 
styleMask: NSTitledWindowMask | NSClosableWindowMask backing:NSBackingStoreNonretained defer:NO];

// grab the dimensions of the viewport of the mobileView, and create a frame for our WebView that matches the dimensions of the document that is loaded.
NSView *viewport = [[[mobileView mainFrame] frameView] documentView]; // width/height of html page
NSRect viewportBounds = [viewport bounds];
NSRect frame = NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height);

// create a new web view to attach to our hidden window.
WebView *hiddenWebView = [[WebView alloc] initWithFrame:frame frameName:@"Hidden.Frame" groupName:nil];

// grab the value of textField as a string
NSString *hURL = [textField stringValue];

// set hiddenWebView as the content of hiddenWindow
[hiddenWindow setContentView:hiddenWebView];

// call loadRequest on hiddenWebView, converting our hURL string to a URLRequest object.
[[hiddenWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:hURL]]];

// we lock our focus on the WebView. This becomes important later when we want to capture this WebView.
[hiddenWebView lockFocus];

// as long as the URL is loading, stick right here.
while ([hiddenWebView isLoading]) {
    [hiddenWebView setNeedsDisplay:NO];
    [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate dateWithTimeIntervalSinceNow:1.0] inMode:NSDefaultRunLoopMode dequeue:YES];
}

// once the loading condition is satisfied we can move on.
[hiddenWebView setNeedsDisplay:YES];

// the first step is to create a bitmap (referenced internally as bitmap) that corresponds to the dimensions of our WebView.
// you can see now why we locked focus on the WebView, as we are creating this bitmap by telling it to look at the FocusedView.
bitmap = [[NSBitmapImageRep alloc] initWithFocusedViewRect:viewportBounds];

// now that we have grabbed our image, lets release the focus on the WebView.
[h iddenWebView unlockFocus];

// convert our bitmap to an image for previewing.
NSImage *dispImage = [[NSImage alloc] initWithData:[bitmap TIFFRepresentation]];

// Display the image by assigning it to an NSImageView.
[imagePreview setImage:dispImage];

}

Okay, you should now have a perfectly captured image of the site you are testing. Next steps would be to create a method to write this image out to disk, or further transform it. Well I hope this was helpful.