feat: screenshot/capture a web content view

This commit is contained in:
Pandelis Zembashis 2026-02-22 05:11:23 +00:00
commit e5806d32bc
8 changed files with 121 additions and 11 deletions

View file

@ -0,0 +1,22 @@
package webcontentsview
import (
"sync"
"sync/atomic"
)
var snapshotCallbacks sync.Map
var snapshotCallbackID uintptr
func registerSnapshotCallback(ch chan string) uintptr {
id := atomic.AddUintptr(&snapshotCallbackID, 1)
snapshotCallbacks.Store(id, ch)
return id
}
func dispatchSnapshotResult(id uintptr, data string) {
if ch, ok := snapshotCallbacks.Load(id); ok {
ch.(chan string) <- data
snapshotCallbacks.Delete(id)
}
}

View file

@ -45,6 +45,10 @@ func (v *WebContentsView) SetURL(url string) {
}
// ExecJS executes the given javascript in the WebContentsView.
func (v *WebContentsView) ExecJS(js string) {
v.impl.execJS(js)
}
// GoBack navigates to the previous page in history.
func (v *WebContentsView) GoBack() {
v.impl.goBack()
@ -55,8 +59,9 @@ func (v *WebContentsView) GetURL() string {
return v.impl.getURL()
}
func (v *WebContentsView) ExecJS(js string) {
v.impl.execJS(js)
// TakeSnapshot returns a base64 encoded PNG of the current view.
func (v *WebContentsView) TakeSnapshot() string {
return v.impl.takeSnapshot()
}
// Attach binds the WebContentsView to a Wails Window.
@ -76,7 +81,7 @@ type webContentsViewImpl interface {
execJS(js string)
goBack()
getURL() string
takeSnapshot() string
attach(window application.Window)
detach()
nativeView() unsafe.Pointer

View file

@ -20,6 +20,8 @@ func (w *androidWebContentsView) setURL(url string) {}
func (w *androidWebContentsView) execJS(js string) {}
func (w *androidWebContentsView) goBack() {}
func (w *androidWebContentsView) getURL() string { return "" }
func (w *androidWebContentsView) takeSnapshot() string { return "" }
func (w *androidWebContentsView) attach(window application.Window) {}
func (w *androidWebContentsView) detach() {}

View file

@ -78,6 +78,15 @@ func (w *macosWebContentsView) goBack() {
C.webContentsViewGoBack(w.nsView)
}
func (w *macosWebContentsView) takeSnapshot() string {
ch := make(chan string, 1)
id := registerSnapshotCallback(ch)
application.InvokeSync(func() {
C.webContentsViewTakeSnapshot(w.nsView, C.uintptr_t(id))
})
return <-ch
}
func (w *macosWebContentsView) getURL() string {
cUrl := C.webContentsViewGetURL(w.nsView)
if cUrl == nil {
@ -110,3 +119,12 @@ func (w *macosWebContentsView) detach() {
func (w *macosWebContentsView) nativeView() unsafe.Pointer {
return w.nsView
}
//export browserViewSnapshotCallback
func browserViewSnapshotCallback(callbackID C.uintptr_t, base64 *C.char) {
id := uintptr(callbackID)
str := ""
if base64 != nil {
str = C.GoString(base64)
}
dispatchSnapshotResult(id, str)
}

View file

@ -23,10 +23,13 @@ extern void* createWebContentsView(int x, int y, int w, int h, WebContentsViewPr
extern void webContentsViewSetBounds(void* view, int x, int y, int w, int h);
extern void webContentsViewSetURL(void* view, const char* url);
extern void webContentsViewExecJS(void* view, const char* js);
extern void webContentsViewGoBack(void* view);
extern const char* webContentsViewGetURL(void* view);
extern void windowAddWebContentsView(void* nsWindow, void* view);
extern void windowRemoveWebContentsView(void* nsWindow, void* view);
// Async snapshot
extern void webContentsViewTakeSnapshot(void* view, uintptr_t callbackID);
#endif /* webcontentsview_darwin_h */
extern void webContentsViewGoBack(void* view);
extern const char* webContentsViewGetURL(void* view);

View file

@ -30,11 +30,11 @@ void* createWebContentsView(int x, int y, int w, int h, WebContentsViewPreferenc
WKWebView* webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
[webView setValue:@(NO) forKey:@"drawsTransparentBackground"];
if (prefs.userAgent != NULL) {
NSString* customUA = [NSString stringWithUTF8String:prefs.userAgent];
[webView setCustomUserAgent:customUA];
}
return webView;
}
@ -44,7 +44,6 @@ void webContentsViewSetBounds(void* view, int x, int y, int w, int h) {
NSView* superview = [webView superview];
if (superview != nil) {
// macOS standard coordinates: 0,0 is bottom-left
CGFloat superHeight = superview.bounds.size.height;
CGFloat cocoaY = superHeight - y - h;
[webView setFrame:NSMakeRect(x, cocoaY, w, h)];
@ -73,11 +72,7 @@ void webContentsViewExecJS(void* view, const char* js) {
void windowAddWebContentsView(void* nsWindow, void* view) {
NSWindow* window = (NSWindow*)nsWindow;
WKWebView* webView = (WKWebView*)view;
// Add directly to the window's root view so we avoid clipping issues
[window.contentView addSubview:webView];
// Force to front by giving it a high zPosition
[webView setWantsLayer:YES];
webView.layer.zPosition = 9999.0;
}
@ -113,3 +108,61 @@ const char* webContentsViewGetURL(void* view) {
}
return result;
}
extern void browserViewSnapshotCallback(uintptr_t callbackID, const char* base64);
void webContentsViewTakeSnapshot(void* view, uintptr_t callbackID) {
WKWebView* webView = (WKWebView*)view;
dispatch_async(dispatch_get_main_queue(), ^{
@try {
if (@available(macOS 10.13, *)) {
WKSnapshotConfiguration *config = [[WKSnapshotConfiguration alloc] init];
[webView takeSnapshotWithConfiguration:config completionHandler:^(NSImage *image, NSError *error) {
if (error != nil || image == nil) {
browserViewSnapshotCallback(callbackID, NULL);
return;
}
@try {
CGImageRef cgRef = [image CGImageForProposedRect:NULL context:nil hints:nil];
if (cgRef == NULL) {
browserViewSnapshotCallback(callbackID, NULL);
return;
}
NSBitmapImageRep *newRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
if (newRep == nil) {
browserViewSnapshotCallback(callbackID, NULL);
return;
}
[newRep setSize:[image size]];
NSData *pngData = [newRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
if (pngData == nil) {
browserViewSnapshotCallback(callbackID, NULL);
return;
}
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
if (base64String == nil) {
browserViewSnapshotCallback(callbackID, NULL);
return;
}
NSString *fullBase64 = [NSString stringWithFormat:@"data:image/png;base64,%@", base64String];
browserViewSnapshotCallback(callbackID, [fullBase64 UTF8String]);
} @catch (NSException *innerException) {
NSLog(@"Error processing snapshot image: %@", innerException.reason);
browserViewSnapshotCallback(callbackID, NULL);
}
}];
} else {
browserViewSnapshotCallback(callbackID, NULL);
}
} @catch (NSException *e) {
NSLog(@"Exception in snapshot configuration: %@", e.reason);
browserViewSnapshotCallback(callbackID, NULL);
}
});
}

View file

@ -20,6 +20,8 @@ func (w *iosWebContentsView) setURL(url string) {}
func (w *iosWebContentsView) execJS(js string) {}
func (w *iosWebContentsView) goBack() {}
func (w *iosWebContentsView) getURL() string { return "" }
func (w *iosWebContentsView) takeSnapshot() string { return "" }
func (w *iosWebContentsView) attach(window application.Window) {}
func (w *iosWebContentsView) detach() {}

View file

@ -115,6 +115,11 @@ func (w *linuxWebContentsView) goBack() {
func (w *linuxWebContentsView) getURL() string {
return ""
}
func (w *linuxWebContentsView) takeSnapshot() string {
return ""
return ""
}
func (w *linuxWebContentsView) execJS(js string) {
cJs := C.CString(js)