chromium/ui/snapshot/snapshot_mac.mm

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/snapshot/snapshot.h"

#import <Cocoa/Cocoa.h>
#import <ScreenCaptureKit/ScreenCaptureKit.h>

#include "base/apple/scoped_cftyperef.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image.h"
#include "ui/snapshot/snapshot_mac.h"

// TODO: Remove when Chromium is built against the macOS 14.4 SDK or newer.
#if !defined(MAC_OS_VERSION_14_4)

@interface SCShareableContent (NewAPI)
+ (void)getCurrentProcessShareableContentWithCompletionHandler:
    (void (^)(SCShareableContent* _Nullable shareableContent,
              NSError* _Nullable error))completionHandler
    API_AVAILABLE(macos(14.4));
@end

@interface SCStreamConfiguration (NewAPI)
@property(nonatomic, assign) BOOL includeChildWindows API_AVAILABLE(macos(14.2))
    ;
@end

#endif  // !defined(MAC_OS_VERSION_14_4)

// The API that allows an app TCC-less access to its own windows is new in macOS
// 14.4. While this has been tested extensively on 14.4 betas, because this is a
// new API added in an OS dot release, have a "break in case of emergency" off-
// switch.
BASE_FEATURE(kUseScreenCaptureKitForSnapshots,
             "UseScreenCaptureKitForSnapshots",
             base::FEATURE_ENABLED_BY_DEFAULT);

namespace ui {

namespace {

SnapshotAPI g_snapshot_api = SnapshotAPI::kUnspecified;

void GrabViewSnapshotScreenCaptureKitImpl(gfx::NativeView native_view,
                                          const gfx::Rect& source_rect,
                                          GrabSnapshotImageCallback callback)
    API_AVAILABLE(macos(14.4)) {
  NSView* view = native_view.GetNativeNSView();
  NSInteger window_number = view.window.windowNumber;
  __block GrabSnapshotImageCallback local_callback = std::move(callback);

  // Get the view frame relative to the window, and flip it to have an
  // upper-left origin. (ScreenCaptureKit works with upper-left origins, as does
  // Views.)
  NSRect view_frame = [view convertRect:view.bounds toView:nil];
  view_frame.origin.y = view.window.frame.size.height - view_frame.origin.y -
                        view_frame.size.height;

  // Offset the `source_rect` to be relative to the view bounds's upper-left
  // origin.
  NSRect clip_rect = source_rect.ToCGRect();
  clip_rect = NSOffsetRect(clip_rect, view_frame.origin.x, view_frame.origin.y);

  [SCShareableContent getCurrentProcessShareableContentWithCompletionHandler:^(
                          SCShareableContent* shareable_content,
                          NSError* error) {
    dispatch_async(dispatch_get_main_queue(), ^{
      if (error) {
        DLOG(ERROR) << base::SysNSStringToUTF8(error.description);
        std::move(local_callback).Run(gfx::Image());
        return;
      }

      // Find the SCWindow corresponding to the view being snapshotted.
      NSArray<SCWindow*>* sc_windows = shareable_content.windows;
      NSUInteger sc_window_index =
          [sc_windows indexOfObjectPassingTest:^BOOL(
                          SCWindow* obj, NSUInteger idx, BOOL* stop) {
            return obj.windowID == window_number;
          }];
      if (sc_window_index == NSNotFound) {
        DLOG(ERROR) << "failed to find window";
        std::move(local_callback).Run(gfx::Image());
        return;
      }
      SCWindow* sc_window = sc_windows[sc_window_index];

      // Build the filter and config for the capture.
      SCContentFilter* filter =
          [[SCContentFilter alloc] initWithDesktopIndependentWindow:sc_window];
      SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init];
      NSSize image_size = clip_rect.size;
      config.width = image_size.width * filter.pointPixelScale;
      config.height = image_size.height * filter.pointPixelScale;
      config.sourceRect = clip_rect;  // In DIPs.
      config.showsCursor = NO;
      config.ignoreShadowsSingleWindow = YES;
      config.captureResolution = SCCaptureResolutionBest;
      config.ignoreGlobalClipSingleWindow = YES;
      config.includeChildWindows = NO;

      [SCScreenshotManager
          captureImageWithFilter:filter
                   configuration:config
               completionHandler:^(CGImageRef sample_buffer, NSError* error2) {
                 // The block below will retain an Objective-C object but not a
                 // CF type, so convert the CGImage to an NSImage before
                 // enqueuing the block.
                 NSImage* image;
                 if (sample_buffer) {
                   // Do not correctly size here. Downstream callers are
                   // assuming that the image returned is scaled by the device
                   // pixel ratio.
                   image = [[NSImage alloc] initWithCGImage:sample_buffer
                                                       size:NSZeroSize];
                 }
                 dispatch_async(dispatch_get_main_queue(), ^{
                   if (error2) {
                     DLOG(ERROR) << base::SysNSStringToUTF8(error2.description);
                     std::move(local_callback).Run(gfx::Image());
                     return;
                   }
                   std::move(local_callback).Run(gfx::Image(image));
                 });
               }];
    });
  }];
}

gfx::Image GrabViewSnapshotCGWindowListImpl(gfx::NativeView native_view,
                                            const gfx::Rect& snapshot_bounds) {
  NSView* view = native_view.GetNativeNSView();
  NSWindow* window = view.window;
  NSScreen* screen = NSScreen.screens.firstObject;
  gfx::Rect screen_bounds = gfx::Rect(NSRectToCGRect(screen.frame));

  // Get the view bounds relative to the screen.
  NSRect frame = [view convertRect:view.bounds toView:nil];
  frame = [window convertRectToScreen:frame];

  gfx::Rect view_bounds = gfx::Rect(NSRectToCGRect(frame));

  // Flip window coordinates based on the primary screen.
  view_bounds.set_y(screen_bounds.height() - view_bounds.y() -
                    view_bounds.height());

  // Convert snapshot bounds relative to window into bounds relative to
  // screen.
  gfx::Rect screen_snapshot_bounds = snapshot_bounds;
  screen_snapshot_bounds.Offset(view_bounds.OffsetFromOrigin());

  DCHECK_LE(screen_snapshot_bounds.right(), view_bounds.right());
  DCHECK_LE(screen_snapshot_bounds.bottom(), view_bounds.bottom());

  base::apple::ScopedCFTypeRef<CGImageRef> window_snapshot(
      CGWindowListCreateImage(
          screen_snapshot_bounds.ToCGRect(), kCGWindowListOptionIncludingWindow,
          window.windowNumber, kCGWindowImageBoundsIgnoreFraming));
  if (!window_snapshot || CGImageGetWidth(window_snapshot.get()) <= 0) {
    return gfx::Image();
  }

  return gfx::Image([[NSImage alloc] initWithCGImage:window_snapshot.get()
                                                size:NSZeroSize]);
}

bool ShouldForceOldAPIUse() {
  // The SCK API +[SCShareableContent
  // getCurrentProcessShareableContentWithCompletionHandler:] was introduced in
  // macOS 14.4, but it did not work correctly when there were multiple
  // instances of an app with the same bundle ID.
  //
  // This is fixed in macOS 15.
  //
  // https://crbug.com/333443445, FB13717818
  if (base::mac::MacOSVersion() > 15'00'00) {
    return false;
  }

  return [NSRunningApplication
             runningApplicationsWithBundleIdentifier:NSBundle.mainBundle
                                                         .bundleIdentifier]
             .count > 1;
}

}  // namespace

void ForceAPIUsageForTesting(SnapshotAPI api) {
  CHECK(base::mac::MacOSVersion() >= 14'04'00 || api != SnapshotAPI::kNewAPI);
  g_snapshot_api = api;
}

void GrabWindowSnapshot(gfx::NativeWindow native_window,
                        const gfx::Rect& source_rect,
                        GrabSnapshotImageCallback callback) {
  // Make sure to grab the "window frame" view so we get current tab +
  // tabstrip.
  NSView* view = native_window.GetNativeNSWindow().contentView.superview;

  GrabViewSnapshot(view, source_rect, std::move(callback));
}

void GrabViewSnapshot(gfx::NativeView view,
                      const gfx::Rect& source_rect,
                      GrabSnapshotImageCallback callback) {
  SnapshotAPI api = g_snapshot_api;
  if (api == SnapshotAPI::kUnspecified) {
    if (base::mac::MacOSVersion() >= 14'04'00 &&
        base::FeatureList::IsEnabled(kUseScreenCaptureKitForSnapshots) &&
        !ShouldForceOldAPIUse()) {
      api = SnapshotAPI::kNewAPI;
    } else {
      api = SnapshotAPI::kOldAPI;
    }
  }

  if (@available(macOS 14.4, *)) {
    if (api == SnapshotAPI::kNewAPI) {
      GrabViewSnapshotScreenCaptureKitImpl(view, source_rect,
                                           std::move(callback));
      return;
    }
  }

  gfx::Image image = GrabViewSnapshotCGWindowListImpl(view, source_rect);
  std::move(callback).Run(image);
}

}  // namespace ui