File: snapshot_mac.mm

package info (click to toggle)
chromium 138.0.7204.183-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,908 kB
  • sloc: cpp: 34,937,088; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (220 lines) | stat: -rw-r--r-- 8,613 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// 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"

// 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(gfx::NativeView(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