blob: 1b38acd21b21562b6977ba49af544d33f55889e1 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/snapshots/snapshot_generator.h"
// TODO(crbug.com/636188): required to implement ViewHierarchyContainsWKWebView
// for -drawViewHierarchyInRect:afterScreenUpdates:, remove once the workaround
// is no longer needed.
#import <WebKit/WebKit.h>
#include <algorithm>
#include "base/bind.h"
#include "base/logging.h"
#include "base/task/post_task.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/snapshots/snapshot_cache.h"
#import "ios/chrome/browser/snapshots/snapshot_cache_factory.h"
#import "ios/chrome/browser/snapshots/snapshot_generator_delegate.h"
#import "ios/chrome/browser/snapshots/snapshot_overlay.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/web/public/web_state/web_state.h"
#import "ios/web/public/web_state/web_state_observer_bridge.h"
#include "ios/web/public/web_task_traits.h"
#include "ios/web/public/web_thread.h"
#include "ui/gfx/image/image.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Returns YES if |view| or any view it contains is a WKWebView.
BOOL ViewHierarchyContainsWKWebView(UIView* view) {
if ([view isKindOfClass:[WKWebView class]])
return YES;
for (UIView* subview in view.subviews) {
if (ViewHierarchyContainsWKWebView(subview))
return YES;
}
return NO;
}
} // namespace
@interface SnapshotGenerator ()<CRWWebStateObserver>
// Property providing access to the snapshot's cache. May be nil.
@property(nonatomic, readonly) SnapshotCache* snapshotCache;
// The unique ID for the web state.
@property(nonatomic, copy) NSString* sessionID;
// The associated web state.
@property(nonatomic, assign) web::WebState* webState;
@end
@implementation SnapshotGenerator {
std::unique_ptr<web::WebStateObserver> _webStateObserver;
}
- (instancetype)initWithWebState:(web::WebState*)webState
snapshotSessionId:(NSString*)snapshotSessionId {
if ((self = [super init])) {
DCHECK(webState);
DCHECK(snapshotSessionId);
_webState = webState;
_sessionID = snapshotSessionId;
_webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserver.get());
}
return self;
}
- (void)dealloc {
if (_webState) {
_webState->RemoveObserver(_webStateObserver.get());
_webStateObserver.reset();
_webState = nullptr;
}
}
- (void)retrieveSnapshot:(void (^)(UIImage*))callback {
DCHECK(callback);
if (self.snapshotCache) {
[self.snapshotCache retrieveImageForSessionID:self.sessionID
callback:callback];
} else {
callback(nil);
}
}
- (void)retrieveGreySnapshot:(void (^)(UIImage*))callback {
DCHECK(callback);
__weak SnapshotGenerator* weakSelf = self;
void (^wrappedCallback)(UIImage*) = ^(UIImage* image) {
if (!image) {
image = [weakSelf updateSnapshot];
if (image)
image = GreyImage(image);
}
callback(image);
};
SnapshotCache* snapshotCache = self.snapshotCache;
if (snapshotCache) {
[snapshotCache retrieveGreyImageForSessionID:self.sessionID
callback:wrappedCallback];
} else {
wrappedCallback(nil);
}
}
- (UIImage*)updateSnapshot {
UIImage* snapshot = [self generateSnapshotWithOverlays:YES];
[self updateSnapshotCacheWithImage:snapshot];
return snapshot;
}
- (void)updateWebViewSnapshotWithCompletion:(void (^)(UIImage*))completion {
DCHECK(self.webState);
UIView* snapshotView = [self.delegate snapshotGenerator:self
baseViewForWebState:self.webState];
CGRect snapshotFrame =
[self.webState->GetView() convertRect:[self snapshotFrame]
fromView:snapshotView];
if (CGRectIsEmpty(snapshotFrame)) {
if (completion) {
base::PostTaskWithTraits(FROM_HERE, {web::WebThread::UI},
base::BindOnce(^{
completion(nil);
}));
}
return;
}
CGSize size = snapshotFrame.size;
DCHECK(std::isnormal(size.width) && (size.width > 0))
<< ": snapshotFrame.size.width=" << size.width;
DCHECK(std::isnormal(size.height) && (size.height > 0))
<< ": snapshotFrame.size.height=" << size.height;
NSArray<SnapshotOverlay*>* overlays =
[self.delegate snapshotGenerator:self
snapshotOverlaysForWebState:self.webState];
[self.delegate snapshotGenerator:self
willUpdateSnapshotForWebState:self.webState];
__weak SnapshotGenerator* weakSelf = self;
self.webState->TakeSnapshot(
snapshotFrame, base::BindOnce(^(const gfx::Image& image) {
UIImage* snapshot = [weakSelf snapshotWithOverlays:overlays
image:image
frame:snapshotFrame];
[weakSelf updateSnapshotCacheWithImage:snapshot];
if (completion)
completion(snapshot);
}));
}
- (UIImage*)generateSnapshotWithOverlays:(BOOL)shouldAddOverlay {
CGRect frame = [self snapshotFrame];
if (CGRectIsEmpty(frame))
return nil;
NSArray<SnapshotOverlay*>* overlays =
shouldAddOverlay ? [self.delegate snapshotGenerator:self
snapshotOverlaysForWebState:self.webState]
: nil;
[self.delegate snapshotGenerator:self
willUpdateSnapshotForWebState:self.webState];
UIView* view = [self.delegate snapshotGenerator:self
baseViewForWebState:self.webState];
UIImage* snapshot = [self generateSnapshotForView:view
withRect:frame
overlays:overlays];
return snapshot;
}
- (void)removeSnapshot {
[self.snapshotCache removeImageWithSessionID:self.sessionID];
}
#pragma mark - Private methods
// Returns the frame of the snapshot. Will return an empty rectangle if the
// WebState is not ready to capture a snapshot.
- (CGRect)snapshotFrame {
// Do not generate a snapshot if web usage is disabled (as the WebState's
// view is blank in that case).
if (!self.webState->IsWebUsageEnabled())
return CGRectZero;
// Do not generate a snapshot if the delegate says the WebState view is
// not ready (this generally mean a placeholder is displayed).
if (self.delegate && ![self.delegate snapshotGenerator:self
canTakeSnapshotForWebState:self.webState])
return CGRectZero;
UIView* view = [self.delegate snapshotGenerator:self
baseViewForWebState:self.webState];
UIEdgeInsets headerInsets = [self.delegate snapshotGenerator:self
snapshotEdgeInsetsForWebState:self.webState];
return UIEdgeInsetsInsetRect(view.bounds, headerInsets);
}
// Takes a snapshot for the supplied view (which should correspond to the given
// type of web view). Returns an autoreleased image cropped and scaled
// appropriately. The image can also contain overlays (if |overlays| is not
// nil and not empty).
- (UIImage*)generateSnapshotForView:(UIView*)view
withRect:(CGRect)rect
overlays:(NSArray<SnapshotOverlay*>*)overlays {
DCHECK(view);
CGSize size = rect.size;
DCHECK(std::isnormal(size.width) && (size.width > 0))
<< ": size.width=" << size.width;
DCHECK(std::isnormal(size.height) && (size.height > 0))
<< ": size.height=" << size.height;
const CGFloat kScale =
std::max<CGFloat>(1.0, [self.snapshotCache snapshotScaleForDevice]);
UIGraphicsBeginImageContextWithOptions(size, YES, kScale);
CGContext* context = UIGraphicsGetCurrentContext();
DCHECK(context);
// TODO(crbug.com/636188): -drawViewHierarchyInRect:afterScreenUpdates: is
// buggy on iOS 8/9/10 (and state is unknown for iOS 11) causing GPU glitches,
// screen redraws during animations, broken pinch to dismiss on tablet, etc.
// For the moment, only use it for WKWebView with depends on it. Remove this
// check and always use -drawViewHierarchyInRect:afterScreenUpdates: once it
// is working correctly in all version of iOS supported.
BOOL useDrawViewHierarchy = ViewHierarchyContainsWKWebView(view);
BOOL snapshotSuccess = YES;
CGContextSaveGState(context);
CGContextTranslateCTM(context, -rect.origin.x, -rect.origin.y);
if (useDrawViewHierarchy) {
snapshotSuccess =
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
} else {
[[view layer] renderInContext:context];
}
if ([overlays count]) {
for (SnapshotOverlay* overlay in overlays) {
// Render the overlay view at the desired offset. It is achieved
// by shifting origin of context because view frame is ignored when
// drawing to context.
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, overlay.yOffset);
// |drawViewHierarchyInRect:| has undefined behavior when the view is not
// in the visible view hierarchy. In practice, when this method is called
// on a view that is part of view controller containment, an
// UIViewControllerHierarchyInconsistency exception will be thrown.
if (useDrawViewHierarchy && overlay.view.window) {
[overlay.view drawViewHierarchyInRect:overlay.view.bounds
afterScreenUpdates:YES];
} else {
[[overlay.view layer] renderInContext:context];
}
CGContextRestoreGState(context);
}
}
UIImage* image = nil;
if (snapshotSuccess)
image = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(context);
UIGraphicsEndImageContext();
return image;
}
// Returns an image of the |image| overlaid with |overlays| with the given
// |frame|.
- (UIImage*)snapshotWithOverlays:(NSArray<SnapshotOverlay*>*)overlays
image:(const gfx::Image&)image
frame:(CGRect)frame {
if (image.IsEmpty())
return nil;
if (overlays.count == 0)
return image.ToUIImage();
CGSize size = frame.size;
DCHECK(std::isnormal(size.width) && (size.width > 0))
<< ": size.width=" << size.width;
DCHECK(std::isnormal(size.height) && (size.height > 0))
<< ": size.height=" << size.height;
const CGFloat kScale =
std::max<CGFloat>(1.0, [self.snapshotCache snapshotScaleForDevice]);
UIGraphicsBeginImageContextWithOptions(size, YES, kScale);
CGContext* context = UIGraphicsGetCurrentContext();
DCHECK(context);
CGContextSaveGState(context);
[image.ToUIImage() drawAtPoint:CGPointZero];
for (SnapshotOverlay* overlay in overlays) {
// Render the overlay view at the desired offset. It is achieved
// by shifting origin of context because view frame is ignored when
// drawing to context.
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, overlay.yOffset - frame.origin.y);
// |drawViewHierarchyInRect:| has undefined behavior when the view is not in
// the visible view hierarchy. In practice, when this method is called on a
// view that is part of view controller containment, an
// UIViewControllerHierarchyInconsistency exception will be thrown.
if (overlay.view.window) {
[overlay.view drawViewHierarchyInRect:overlay.view.bounds
afterScreenUpdates:YES];
} else {
[[overlay.view layer] renderInContext:context];
}
CGContextRestoreGState(context);
}
UIImage* snapshotWithOverlays = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(context);
UIGraphicsEndImageContext();
return snapshotWithOverlays;
}
// Updates the snapshot cache with |snapshot|.
- (void)updateSnapshotCacheWithImage:(UIImage*)snapshot {
if (snapshot) {
[self.snapshotCache setImage:snapshot withSessionID:self.sessionID];
} else {
// Remove any stale snapshot since the snapshot failed.
[self.snapshotCache removeImageWithSessionID:self.sessionID];
}
}
#pragma mark - Properties
- (SnapshotCache*)snapshotCache {
return SnapshotCacheFactory::GetForBrowserState(
ios::ChromeBrowserState::FromBrowserState(
self.webState->GetBrowserState()));
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
_webState->RemoveObserver(_webStateObserver.get());
_webStateObserver.reset();
_webState = nullptr;
}
@end