WebView2Feedback icon indicating copy to clipboard operation
WebView2Feedback copied to clipboard

WinUI RenderAsync method doesn't work for making screenshots of WebView2

Open Eilon opened this issue 3 years ago • 3 comments

Description

In WinUI you can call RenderTargetBitmap.RenderAsync(element) to render a screenshot of an element to a bitmap, and then you can save that bitmap somewhere (for example, as a PNG on disk).

When there's a WebView2 in the control tree, the contents of the WebView2 are completely blank, though everything else in the image looks fine.

Version SDK: Windows App SDK 1.1.5 Runtime: 105.0.1343.33 Framework: WinUI OS: Win11

Repro Steps

Create a new empty WinUI3 app and put this code in MainWindow.xaml.cs:

    public sealed partial class MainWindow : Window
    {
        WebView2 _wv;
        public MainWindow()
        {
            var b = new Button { Content = new TextBlock { Text = "Do screenshot" } };
            b.Click += OnScreenshotClick;

            _wv = new WebView2 { Source = new Uri("https://bing.com"), Height=400, Width=400 };

            var sp = new StackPanel() { Orientation = Orientation.Vertical };
            sp.Children.Add(b);
            sp.Children.Add(_wv);
            Content = sp;
        }

        private async void OnScreenshotClick(object sender, RoutedEventArgs e)
        {
            var rootPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            await CaptureAsync(Content, Path.Combine(rootPath, "WindowContent.png"));
            await CaptureAsync(_wv, Path.Combine(rootPath, "WebView.png"));

            var messageDialog = new MessageDialog($"Screenshots saved to: {rootPath}");
            messageDialog.Commands.Add(new UICommand("OK"));
            InitializeWithWindow.Initialize(messageDialog, WindowNative.GetWindowHandle(this));
            await messageDialog.ShowAsync();
        }

        public static async Task CaptureAsync(UIElement element, string destination)
        {
            var bmp = new RenderTargetBitmap();

            await bmp.RenderAsync(element);

            // get the view information first
            var width = bmp.PixelWidth;
            var height = bmp.PixelHeight;

            // then potentially move to a different thread
            var pixels = await bmp.GetPixelsAsync();

            using FileStream fileStream = new FileStream(destination, FileMode.Create);

            var ms = fileStream.AsRandomAccessStream();

            var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, ms);
            encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)width, (uint)height, 96, 96, pixels.ToArray());
            await encoder.FlushAsync();
        }
    }

And then run the app, and click the button in the app.

On my machine it produces two screenshots:

  1. Just the WebView2 (blank rectangle):
    WebView
  2. The whole app's main window's contents, which has content, except for the blank WebView2 (valid screenshot, except bottom right is blank gray rectangle):
    WindowContent

Originally reported in .NET MAUI: https://github.com/dotnet/maui/issues/9718

AB#41360109

Eilon avatar Sep 16 '22 00:09 Eilon

@Eilon The WebView2 provides a "CapturePreviewAsync" function which will create a PNG of the web contents. Sounds like the WinUI control would need to incorporate that (or some other mechanism) for RenderAsync. I'm opening a bug for this issue, and we'll ask the WinUI team to take a first look. Thanks!

champnic avatar Sep 16 '22 22:09 champnic

I do believe that this is an MAUI issue, but since that one has been closed and locked, I will post my workaround here:

I have implemented this for Windows, Android and iOS:

        public async Task<byte[]> GetScreenshot()
        {
#if WINDOWS
            using var ms = new MemoryStream();
            var webview = Handler.PlatformView as WebView2;
            await webview.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Jpeg, ms.AsRandomAccessStream());
            return ms.ToArray();
#elif ANDROID
            using var ms = new MemoryStream();
            var webview = (Android.Webkit.WebView)Handler.PlatformView!;
            var bitmap = Bitmap.CreateBitmap(webview.Width, webview.Height, Bitmap.Config.Argb8888!)!;
            var canvas = new Canvas(bitmap);
            webview.Draw(canvas);
            await bitmap.CompressAsync(Bitmap.CompressFormat.Jpeg, 90, ms);
            return ms.ToArray();
#elif IOS
            var webview = (WKWebView)Handler.PlatformView!;
            UIGraphics.BeginImageContextWithOptions(webview.Bounds.Size, true, 0);
            webview.DrawViewHierarchy(webview.Bounds, true);
            var jpegData = UIGraphics.GetImageFromCurrentImageContext().AsJPEG();
            byte[] dataBytes = new byte[jpegData.Length];
            System.Runtime.InteropServices.Marshal.Copy(jpegData.Bytes, dataBytes, 0, Convert.ToInt32(jpegData.Length));
            UIGraphics.EndImageContext();
            return dataBytes;
#endif
            return null;
        }

Here are the necessary usings:

#if IOS
using UIKit;
using WebKit;
#endif

#if ANDROID
using Android.Graphics;
using Path = System.IO.Path;
#endif

#if WINDOWS
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
#endif

If you would like to use this now you can make your own class that inherits from BlazorWebView and add the above method to it.

MaxwellDAssistek avatar Feb 21 '23 19:02 MaxwellDAssistek

Any progress on this?

j-y-c avatar Mar 17 '25 07:03 j-y-c