| // Copyright 2023 The Chromium Authors |
| // 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/model/legacy_snapshot_generator.h" |
| |
| #import "base/debug/crash_logging.h" |
| #import "base/debug/dump_without_crashing.h" |
| #import "base/functional/bind.h" |
| #import "build/blink_buildflags.h" |
| #import "ios/chrome/browser/snapshots/model/model_swift.h" |
| #import "ios/chrome/browser/snapshots/model/snapshot_scale.h" |
| #import "ios/chrome/browser/snapshots/model/web_state_snapshot_info.h" |
| #import "ios/web/public/thread/web_thread.h" |
| #import "ios/web/public/web_client.h" |
| #import "ios/web/public/web_state.h" |
| |
| namespace { |
| |
| // Contains information needed for snapshotting. |
| struct SnapshotInfo { |
| UIView* baseView; |
| CGRect snapshotFrameInBaseView; |
| CGRect snapshotFrameInWindow; |
| }; |
| |
| } // namespace |
| |
| @implementation LegacySnapshotGenerator { |
| // The associated WebState. |
| base::WeakPtr<web::WebState> _webState; |
| } |
| |
| - (instancetype)initWithWebState:(web::WebState*)webState { |
| if ((self = [super init])) { |
| DCHECK(webState); |
| _webState = webState->GetWeakPtr(); |
| } |
| return self; |
| } |
| |
| - (void)generateSnapshotWithCompletion:(void (^)(UIImage*))completion { |
| bool showing_native_content = |
| web::GetWebClient()->IsAppSpecificURL(_webState->GetLastCommittedURL()); |
| if (!showing_native_content && _webState->CanTakeSnapshot()) { |
| // Take the snapshot using the optimized WKWebView snapshotting API for |
| // pages loaded in the web view when the WebState snapshot API is available. |
| [self generateWKWebViewSnapshotWithCompletion:completion]; |
| return; |
| } |
| // Use the UIKit-based snapshot API as a fallback when the WKWebView API is |
| // unavailable. |
| UIImage* snapshot = [self generateUIViewSnapshotWithOverlays]; |
| if (completion) { |
| completion(snapshot); |
| } |
| } |
| |
| - (UIImage*)generateUIViewSnapshot { |
| if (![self canTakeSnapshot] || !_webState) { |
| return nil; |
| } |
| [_delegate |
| willUpdateSnapshotWithWebStateInfo:[[WebStateSnapshotInfo alloc] |
| initWithWebState:_webState.get()]]; |
| |
| std::optional<SnapshotInfo> snapshotInfo = [self snapshotInfo]; |
| if (!snapshotInfo) { |
| return nil; |
| } |
| // Ideally, generate an UIImage by one step with `UIGraphicsImageRenderer`, |
| // however, it generates a black image when the size of `baseView` is larger |
| // than `frameInBaseView`. So this is a workaround to generate an UIImage by |
| // dividing the step into 2 steps; 1) convert an UIView to an UIImage 2) crop |
| // an UIImage with `frameInBaseView`. |
| UIImage* baseImage = [self convertFromBaseView:snapshotInfo.value().baseView]; |
| return [self cropImage:baseImage |
| frameInBaseView:snapshotInfo.value().snapshotFrameInBaseView]; |
| } |
| |
| - (UIImage*)generateUIViewSnapshotWithOverlays { |
| if (![self canTakeSnapshot]) { |
| return nil; |
| } |
| std::optional<SnapshotInfo> snapshotInfo = [self snapshotInfo]; |
| if (!snapshotInfo) { |
| return nil; |
| } |
| return [self addOverlays:[self overlays] |
| baseImage:[self generateUIViewSnapshot] |
| frameInWindow:snapshotInfo.value().snapshotFrameInWindow]; |
| } |
| |
| #pragma mark - Private methods |
| |
| // Asynchronously generates a new snapshot with WebKit-based snapshot API and |
| // runs a callback with the new snapshot image. It is an error to call this |
| // method if the web state is showing anything other (e.g., native content) than |
| // a web view. |
| - (void)generateWKWebViewSnapshotWithCompletion:(void (^)(UIImage*))completion { |
| if (!_webState) { |
| return; |
| } |
| DCHECK( |
| !web::GetWebClient()->IsAppSpecificURL(_webState->GetLastCommittedURL())); |
| |
| if (![self canTakeSnapshot]) { |
| if (completion) { |
| // Post a task to the current thread (UI thread). |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(completion, nil)); |
| } |
| return; |
| } |
| [_delegate |
| willUpdateSnapshotWithWebStateInfo:[[WebStateSnapshotInfo alloc] |
| initWithWebState:_webState.get()]]; |
| |
| std::optional<SnapshotInfo> snapshotInfo = [self snapshotInfo]; |
| if (!snapshotInfo) { |
| return; |
| } |
| auto wrappedCompletion = |
| ^(__weak LegacySnapshotGenerator* generator, UIImage* image) { |
| if (!generator) { |
| completion(nil); |
| } |
| UIImage* snapshot = |
| [generator addOverlays:[generator overlays] |
| baseImage:image |
| frameInWindow:snapshotInfo.value().snapshotFrameInWindow]; |
| if (completion) { |
| completion(snapshot); |
| } |
| }; |
| |
| __weak LegacySnapshotGenerator* weakSelf = self; |
| _webState->TakeSnapshot(snapshotInfo.value().snapshotFrameInBaseView, |
| base::BindRepeating(wrappedCompletion, weakSelf)); |
| } |
| |
| |
| // 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 (!_delegate || !_webState) { |
| return NO; |
| } |
| |
| // Do not generate a snapshot if web usage is disabled (as the WebState's |
| // view is blank in that case). |
| if (!_webState->IsWebUsageEnabled()) { |
| return NO; |
| } |
| |
| return [_delegate |
| canTakeSnapshotWithWebStateInfo:[[WebStateSnapshotInfo alloc] |
| initWithWebState:_webState.get()]]; |
| } |
| |
| // Converts an UIView to an UIImage. The size of generated UIImage is the same |
| // as `baseView`. |
| - (UIImage*)convertFromBaseView:(UIView*)baseView { |
| DCHECK(baseView); |
| |
| // Disable the automatic view dimming UIKit performs if a view is presented |
| // modally over `baseView`. |
| baseView.tintAdjustmentMode = UIViewTintAdjustmentModeNormal; |
| |
| // Note: When not using device scale, the output image size may slightly |
| // differ from the input size due to rounding. |
| const CGFloat kScale = [SnapshotImageScale floatImageScaleForDevice]; |
| DCHECK_GE(kScale, 1.0); |
| UIGraphicsImageRendererFormat* format = |
| [UIGraphicsImageRendererFormat preferredFormat]; |
| format.scale = kScale; |
| format.opaque = YES; |
| |
| UIGraphicsImageRenderer* renderer = |
| [[UIGraphicsImageRenderer alloc] initWithBounds:baseView.bounds |
| format:format]; |
| |
| __block BOOL snapshotSuccess = YES; |
| UIImage* image = |
| [renderer imageWithActions:^(UIGraphicsImageRendererContext* UIContext) { |
| // Render the view's layer via `-renderInContext:`. |
| // To mitigate against crashes like crbug.com/1429512, ensure that |
| // the layer's position is valid. If not, mark the snapshotting as |
| // failed. |
| CALayer* layer = baseView.layer; |
| CGPoint pos = layer.position; |
| if (isnan(pos.x) || isnan(pos.y)) { |
| snapshotSuccess = NO; |
| } else { |
| [layer renderInContext:UIContext.CGContext]; |
| } |
| }]; |
| |
| if (!snapshotSuccess) { |
| image = nil; |
| } |
| |
| // Set the mode to UIViewTintAdjustmentModeAutomatic. |
| baseView.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic; |
| |
| return image; |
| } |
| |
| // Crops an UIImage to `frameInBaseView`. |
| - (UIImage*)cropImage:(UIImage*)baseImage |
| frameInBaseView:(CGRect)frameInBaseView { |
| if (!baseImage) { |
| return nil; |
| } |
| DCHECK(!CGRectIsEmpty(frameInBaseView)); |
| |
| // Scale `frameInBaseView` to handle an image with 2x scale. |
| CGFloat scale = baseImage.scale; |
| frameInBaseView.origin.x *= scale; |
| frameInBaseView.origin.y *= scale; |
| frameInBaseView.size.width *= scale; |
| frameInBaseView.size.height *= scale; |
| |
| // Perform cropping. |
| CGImageRef imageRef = |
| CGImageCreateWithImageInRect(baseImage.CGImage, frameInBaseView); |
| |
| // Convert back to an UIImage. |
| UIImage* image = [UIImage imageWithCGImage:imageRef |
| scale:scale |
| orientation:baseImage.imageOrientation]; |
| |
| // Clean up a reference pointer. |
| CGImageRelease(imageRef); |
| |
| return image; |
| } |
| |
| // Returns an image of the `baseImage` overlaid with `overlays` with the given |
| // `frameInWindow`. |
| - (UIImage*)addOverlays:(NSArray<UIView*>*)overlays |
| baseImage:(UIImage*)baseImage |
| frameInWindow:(CGRect)frameInWindow { |
| DCHECK(!CGRectIsEmpty(frameInWindow)); |
| if (!baseImage) { |
| return nil; |
| } |
| // Note: If the baseImage scale differs from device scale, the baseImage size |
| // may slightly differ from frameInWindow size due to rounding. Do not attempt |
| // to compare the baseImage size and frameInWindow size. |
| if (overlays.count == 0) { |
| return baseImage; |
| } |
| const CGFloat kScale = [SnapshotImageScale floatImageScaleForDevice]; |
| DCHECK_GE(kScale, 1.0); |
| |
| UIGraphicsImageRendererFormat* format = |
| [UIGraphicsImageRendererFormat preferredFormat]; |
| format.scale = kScale; |
| format.opaque = YES; |
| |
| UIGraphicsImageRenderer* renderer = |
| [[UIGraphicsImageRenderer alloc] initWithSize:frameInWindow.size |
| format:format]; |
| |
| return |
| [renderer imageWithActions:^(UIGraphicsImageRendererContext* UIContext) { |
| CGContextRef context = UIContext.CGContext; |
| |
| // The base image is already a cropped snapshot so it is drawn at the |
| // origin of the new image. |
| [baseImage drawInRect:(CGRect){.origin = CGPointZero, |
| .size = frameInWindow.size}]; |
| |
| // This shifts the origin of the context so that future drawings can be |
| // in window coordinates. For example, suppose that the desired snapshot |
| // area is at (0, 99) in the window coordinate space. Drawing at (0, 99) |
| // will appear as (0, 0) in the resulting image. |
| CGContextTranslateCTM(context, -frameInWindow.origin.x, |
| -frameInWindow.origin.y); |
| [self drawOverlays:overlays context:context]; |
| }]; |
| } |
| |
| // Draws `overlays` onto `context` at offsets relative to the window. |
| - (void)drawOverlays:(NSArray<UIView*>*)overlays context:(CGContext*)context { |
| for (UIView* overlay in overlays) { |
| CGContextSaveGState(context); |
| CGRect frameInWindow = [overlay.superview convertRect:overlay.frame |
| toView:nil]; |
| // This shifts the context so that drawing starts at the overlay's offset. |
| CGContextTranslateCTM(context, frameInWindow.origin.x, |
| frameInWindow.origin.y); |
| [[overlay layer] renderInContext:context]; |
| CGContextRestoreGState(context); |
| } |
| } |
| |
| // Retrieves the overlays laid down on the WebState. |
| - (NSArray<UIView*>*)overlays { |
| if (!_webState) { |
| return nil; |
| } |
| return [_delegate |
| snapshotOverlaysWithWebStateInfo:[[WebStateSnapshotInfo alloc] |
| initWithWebState:_webState.get()]]; |
| } |
| |
| // Retrieves information needed for snapshotting. |
| - (std::optional<SnapshotInfo>)snapshotInfo { |
| CHECK(_webState); |
| SnapshotInfo snapshotInfo; |
| snapshotInfo.baseView = [_delegate |
| baseViewWithWebStateInfo:[[WebStateSnapshotInfo alloc] |
| initWithWebState:_webState.get()]]; |
| DCHECK(snapshotInfo.baseView); |
| |
| UIEdgeInsets baseViewInsets = [_delegate |
| snapshotEdgeInsetsWithWebStateInfo:[[WebStateSnapshotInfo alloc] |
| initWithWebState:_webState.get()]]; |
| snapshotInfo.snapshotFrameInBaseView = |
| UIEdgeInsetsInsetRect(snapshotInfo.baseView.bounds, baseViewInsets); |
| if (CGRectIsEmpty(snapshotInfo.snapshotFrameInBaseView)) { |
| return std::nullopt; |
| } |
| |
| snapshotInfo.snapshotFrameInWindow = |
| [snapshotInfo.baseView convertRect:snapshotInfo.snapshotFrameInBaseView |
| toView:nil]; |
| if (CGRectIsEmpty(snapshotInfo.snapshotFrameInWindow)) { |
| return std::nullopt; |
| } |
| return snapshotInfo; |
| } |
| |
| @end |