blob: 7091f0dc996d30f8b40443cf6db1c286ca906c0c [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"
#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"
#include "ios/chrome/browser/ui/ui_feature_flags.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
@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->ContentIsHTML());
if (![self canTakeSnapshot]) {
if (completion) {
base::PostTaskWithTraits(FROM_HERE, {web::WebThread::UI},
base::BindOnce(^{
completion(nil);
}));
}
return;
}
UIView* snapshotView = [self.delegate snapshotGenerator:self
baseViewForWebState:self.webState];
CGRect snapshotFrame =
[self.webState->GetView() convertRect:[self snapshotFrame]
fromView:snapshotView];
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
baseImage:image
frame:snapshotFrame];
[weakSelf updateSnapshotCacheWithImage:snapshot];
if (completion)
completion(snapshot);
}));
}
- (UIImage*)generateSnapshotWithOverlays:(BOOL)shouldAddOverlay {
if (![self canTakeSnapshot])
return nil;
NSArray<SnapshotOverlay*>* overlays =
shouldAddOverlay ? [self.delegate snapshotGenerator:self
snapshotOverlaysForWebState:self.webState]
: nil;
UIView* view = [self.delegate snapshotGenerator:self
baseViewForWebState:self.webState];
[self.delegate snapshotGenerator:self
willUpdateSnapshotForWebState:self.webState];
return [self snapshotWithOverlays:overlays
baseView:view
frame:[self snapshotFrame]];
}
- (void)removeSnapshot {
[self.snapshotCache removeImageWithSessionID:self.sessionID];
}
#pragma mark - Private methods
// Returns NO if WebState or the view is not ready for snapshot.
- (BOOL)canTakeSnapshot {
// This allows for easier unit testing of classes that use SnapshotGenerator.
if (!self.delegate)
return NO;
// 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 NO;
return [self.delegate snapshotGenerator:self
canTakeSnapshotForWebState:self.webState];
}
// Returns the frame of the snapshot.
- (CGRect)snapshotFrame {
UIView* view = [self.delegate snapshotGenerator:self
baseViewForWebState:self.webState];
UIEdgeInsets headerInsets = [self.delegate snapshotGenerator:self
snapshotEdgeInsetsForWebState:self.webState];
CGRect frame = UIEdgeInsetsInsetRect(view.bounds, headerInsets);
DCHECK(!CGRectIsEmpty(frame));
return frame;
}
// Returns an image of the |view| overlaid with |overlays| with the given
// |frame|.
- (UIImage*)snapshotWithOverlays:(NSArray<SnapshotOverlay*>*)overlays
baseView:(UIView*)view
frame:(CGRect)frame {
DCHECK(view);
DCHECK(!CGRectIsEmpty(frame));
const CGFloat kScale =
std::max<CGFloat>(1.0, [self.snapshotCache snapshotScaleForDevice]);
UIGraphicsBeginImageContextWithOptions(frame.size, YES, kScale);
CGContext* context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, -frame.origin.x, -frame.origin.y);
BOOL snapshotSuccess = YES;
if (base::FeatureList::IsEnabled(kSnapshotDrawView)) {
snapshotSuccess =
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
} else {
[[view layer] renderInContext:context];
}
[self drawOverlays:overlays context:context];
UIImage* image = nil;
if (snapshotSuccess)
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
// Returns an image of the |image| overlaid with |overlays| with the given
// |frame|.
- (UIImage*)snapshotWithOverlays:(NSArray<SnapshotOverlay*>*)overlays
baseImage:(const gfx::Image&)image
frame:(CGRect)frame {
DCHECK(!CGRectIsEmpty(frame));
if (image.IsEmpty())
return nil;
if (overlays.count == 0)
return image.ToUIImage();
const CGFloat kScale =
std::max<CGFloat>(1.0, [self.snapshotCache snapshotScaleForDevice]);
UIGraphicsBeginImageContextWithOptions(frame.size, YES, kScale);
CGContext* context = UIGraphicsGetCurrentContext();
[image.ToUIImage() drawAtPoint:CGPointZero];
[self drawOverlays:overlays context:context];
UIImage* snapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshot;
}
// 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];
}
}
// Draws |overlays| onto |context|.
- (void)drawOverlays:(NSArray<SnapshotOverlay*>*)overlays
context:(CGContext*)context {
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 (base::FeatureList::IsEnabled(kSnapshotDrawView) &&
overlay.view.window) {
[overlay.view drawViewHierarchyInRect:overlay.view.bounds
afterScreenUpdates:YES];
} else {
[[overlay.view layer] renderInContext:context];
}
CGContextRestoreGState(context);
}
}
#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