简体   繁体   中英

C# - Selenium full page screenshot

I'd like to take a full page screenshot using C# with Selenium and ChromeDriver. Here: https://stackoverflow.com/a/45201692/5400125 I found an example how to do it in Java. I am trying to achieve this in C#, but I get an exception after page is loaded on the first call to sendEvaluate:

OpenQA.Selenium.WebDriverException: 'no such session (Driver info: chromedriver=2.41.578737 (49da6702b16031c40d63e5618de03a32ff6c197e),platform=Windows NT 10.0.17134 x86_64)'

public class ChromeDriverEx : ChromeDriver
{
    public ChromeDriverEx(string chromeDriverDirectory, ChromeOptions options)
        : base(chromeDriverDirectory, options, RemoteWebDriver.DefaultCommandTimeout)
    {
        var addCmd = this.GetType().BaseType
            .GetMethod("AddCustomChromeCommand", BindingFlags.NonPublic | BindingFlags.Instance);
        addCmd.Invoke(this,
            new object[] {"sendCommand", "POST", "/session/:sessionId/chromium/send_command_and_get_result"});
    }

    public void GetFullScreenshot()
    {
        Object metrics = sendEvaluate(
            @"({" +
            "width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0," +
            "height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0," +
            "deviceScaleFactor: window.devicePixelRatio || 1," +
            "mobile: typeof window.orientation !== 'undefined'" +
            "})");
    }

    private object sendEvaluate(string script)
    {
        var response = sendCommand("Runtime.evaulate",
            new Dictionary<string, object> {{"returnByValue", true}, {"expression", script}});
        return response;
    }

    private object sendCommand(string cmd, object param)
    {
        var r = this.Execute("sendCommand", new Dictionary<string, object> {{"cmd", cmd}, {"params", param}});
        return r.Value;
    }
}

And I call it like this:

 var opts = new ChromeOptions();
 opts.AddAdditionalCapability("useAutomationExtension", false);
 opts.AddArgument("disable-infobars");
 var driver = new ChromeDriverEx(".", opts);
 driver.Navigate().GoToUrl("https://stackoverflow.com/questions");
 driver.GetFullScreenshot();

I'm using Chrome 68 and ChromeDriver 2.41

This code works fine for me, in creating a subclass of ChromeDriver . Note that the code below is purposely written in a very, very verbose style, so as to clearly illustrate every piece of the solution. It could easily be written more concisely, depending on one's coding style and requirement for robust error handling. Moreover, in a future release, it will be unnecessary to create a method for executing a DevTools command that returns a result; such a method will already be part of the .NET bindings.

public class ChromeDriverEx : ChromeDriver
{
    private const string SendChromeCommandWithResult = "sendChromeCommandWithResponse";
    private const string SendChromeCommandWithResultUrlTemplate = "/session/{sessionId}/chromium/send_command_and_get_result";

    public ChromeDriverEx(string chromeDriverDirectory, ChromeOptions options)
        : base(chromeDriverDirectory, options)
    {
        CommandInfo commandInfoToAdd = new CommandInfo(CommandInfo.PostCommand, SendChromeCommandWithResultUrlTemplate);
        this.CommandExecutor.CommandInfoRepository.TryAddCommand(SendChromeCommandWithResult, commandInfoToAdd);
    }

    public Screenshot GetFullPageScreenshot()
    {
        // Evaluate this only to get the object that the
        // Emulation.setDeviceMetricsOverride command will expect.
        // Note that we can use the already existing ExecuteChromeCommand
        // method to set and clear the device metrics, because there's no
        // return value that we care about.
        string metricsScript = @"({
width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0,
height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0,
deviceScaleFactor: window.devicePixelRatio || 1,
mobile: typeof window.orientation !== 'undefined'
})";
        Dictionary<string, object> metrics = this.EvaluateDevToolsScript(metricsScript);
        this.ExecuteChromeCommand("Emulation.setDeviceMetricsOverride", metrics);

        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["format"] = "png";
        parameters["fromSurface"] = true;
        object screenshotObject = this.ExecuteChromeCommandWithResult("Page.captureScreenshot", parameters);
        Dictionary<string, object> screenshotResult = screenshotObject as Dictionary<string, object>;
        string screenshotData = screenshotResult["data"] as string;

        this.ExecuteChromeCommand("Emulation.clearDeviceMetricsOverride", new Dictionary<string, object>());

        Screenshot screenshot = new Screenshot(screenshotData);
        return screenshot;
    }

    public object ExecuteChromeCommandWithResult(string commandName, Dictionary<string, object> commandParameters)
    {
        if (commandName == null)
        {
            throw new ArgumentNullException("commandName", "commandName must not be null");
        }

        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["cmd"] = commandName;
        parameters["params"] = commandParameters;
        Response response = this.Execute(SendChromeCommandWithResult, parameters);
        return response.Value;
    }

    private Dictionary<string, object> EvaluateDevToolsScript(string scriptToEvaluate)
    {
        // This code is predicated on knowing the structure of the returned
        // object as the result. In this case, we know that the object returned
        // has a "result" property which contains the actual value of the evaluated
        // script, and we expect the value of that "result" property to be an object
        // with a "value" property. Moreover, we are assuming the result will be
        // an "object" type (which translates to a C# Dictionary<string, object>).
        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["returnByValue"] = true;
        parameters["expression"] = scriptToEvaluate;
        object evaluateResultObject = this.ExecuteChromeCommandWithResult("Runtime.evaluate", parameters);
        Dictionary<string, object> evaluateResultDictionary = evaluateResultObject as Dictionary<string, object>;
        Dictionary<string, object> evaluateResult = evaluateResultDictionary["result"] as Dictionary<string, object>;

        // If we wanted to make this actually robust, we'd check the "type" property
        // of the result object before blindly casting to a dictionary.
        Dictionary<string, object> evaluateValue = evaluateResult["value"] as Dictionary<string, object>;
        return evaluateValue;
    }
}

You would use this code with something like the following:

ChromeOptions options = new ChromeOptions();
ChromeDriverEx driver = new ChromeDriverEx(@"C:\path\to\directory\of\chromedriver", options);
driver.Url = "https://stackoverflow.com/questions";

Screenshot screenshot = driver.GetFullPageScreenshot();
screenshot.SaveAsFile(@"C:\desired\screenshot\path\FullPageScreenshot.png");

Here is my sample of get Full Screen ScreenShot:

            string _currentPath = Path.GetDirectoryName(Assembly.GetAssembly(typeof(One of your objects)).Location) + @"\Attachs\";
            var filePath = _currentPath + sSName;

            if (!Directory.Exists(_currentPath))
                Directory.CreateDirectory(_currentPath);

            Dictionary<string, Object> metrics = new Dictionary<string, Object>();
            metrics["width"] = _driver.ExecuteScript("return Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)");
            metrics["height"] = _driver.ExecuteScript("return Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)");
            metrics["deviceScaleFactor"] = (double)_driver.ExecuteScript("return window.devicePixelRatio");
            metrics["mobile"] = _driver.ExecuteScript("return typeof window.orientation !== 'undefined'");
            _driver.ExecuteChromeCommand("Emulation.setDeviceMetricsOverride", metrics);

            _driver.GetScreenshot().SaveAsFile(filePath, ScreenshotImageFormat.Png);

            _driver.ExecuteChromeCommand("Emulation.clearDeviceMetricsOverride", new Dictionary<string, Object>());
            _driver.Close();

I have updated JimEvans solution to also work with pages longer than 8192/16384px by taking multiple screenshots and the stitching them together.

The 16384px limit stems from the maximum texture size used by the compositor.

    public class ChromeDriverEx : ChromeDriver
{
    private const string SendChromeCommandWithResult = "sendChromeCommandWithResponse";
    private const string SendChromeCommandWithResultUrlTemplate = "/session/{sessionId}/chromium/send_command_and_get_result";

    public ChromeDriverEx(ChromeDriverService service, ChromeOptions options)
        : base(service, options)
    {
        CommandInfo commandInfoToAdd = new CommandInfo(CommandInfo.PostCommand, SendChromeCommandWithResultUrlTemplate);
        this.CommandExecutor.CommandInfoRepository.TryAddCommand(SendChromeCommandWithResult, commandInfoToAdd);
    }

    public OpenQA.Selenium.Screenshot GetFullPageScreenshot()
    {
        // Evaluate this only to get the object that the
        // Emulation.setDeviceMetricsOverride command will expect.
        // Note that we can use the already existing ExecuteChromeCommand
        // method to set and clear the device metrics, because there's no
        // return value that we care about.
        string metricsScript = @"({
        width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0,
        height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0,
        deviceScaleFactor: window.devicePixelRatio || 1,,
        mobile: typeof window.orientation !== 'undefined'
        })";
        Dictionary<string, object> metrics = this.EvaluateDevToolsScript(metricsScript);
        this.ExecuteChromeCommand("Emulation.setDeviceMetricsOverride", metrics);
        Thread.Sleep(1000);
        Dictionary<string, object> parameters = new Dictionary<string, object>
        {
            ["format"] = "png",
            ["fromSurface"] = true
        };
        var fullHeight = int.Parse(metrics["height"]?.ToString() ?? "0");
        var splitSSAt = 8192;

        if (fullHeight > splitSSAt)
        {
            var currentHeight = splitSSAt;
            var startHeight = 0;
            List<Bitmap> bitmaps = new List<Bitmap>();

            while (fullHeight > 0)
            {
                if (currentHeight > fullHeight)
                {
                    currentHeight = fullHeight;
                }
                parameters["clip"] = new Dictionary<string, object>
                {
                    ["x"] = 0,
                    ["y"] = startHeight,
                    ["width"] = metrics["width"],
                    ["height"] = currentHeight,
                    ["scale"] = 1,
                };

                object splitScreenshotObject = this.ExecuteChromeCommandWithResult("Page.captureScreenshot", parameters);
                Dictionary<string, object> splitScreenshotResult = splitScreenshotObject as Dictionary<string, object>;
                Byte[] bitmapData = Convert.FromBase64String(FixBase64ForImage(splitScreenshotResult["data"] as string));
                MemoryStream streamBitmap = new System.IO.MemoryStream(bitmapData);
                bitmaps.Add(new Bitmap((Bitmap)Image.FromStream(streamBitmap)));
                fullHeight -= splitSSAt;
                startHeight += splitSSAt;
            }

            using var ms = new MemoryStream();
            using var bitmap = new Bitmap(MergeImages(bitmaps));
            bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
            var base64 = Convert.ToBase64String(ms.GetBuffer()); //Get Base64
            return new OpenQA.Selenium.Screenshot(base64);
        }
        object screenshotObject = this.ExecuteChromeCommandWithResult("Page.captureScreenshot", parameters);
        Dictionary<string, object> screenshotResult = screenshotObject as Dictionary<string, object>;
        string screenshotData = screenshotResult["data"] as string;

        this.ExecuteChromeCommand("Emulation.clearDeviceMetricsOverride", new Dictionary<string, object>());

        var screenshot = new OpenQA.Selenium.Screenshot(screenshotData);
        return screenshot;
    }

    public string FixBase64ForImage(string image)
    {
        StringBuilder sbText = new StringBuilder(image, image.Length);
        sbText.Replace("\r\n", String.Empty); sbText.Replace(" ", string.Empty);
        return sbText.ToString();
    }

    private Bitmap MergeImages(IEnumerable<Bitmap> images)
    {
        var enumerable = images as IList<Bitmap> ?? images.ToList();

        var width = 0;
        var height = 0;

        foreach (var image in enumerable)
        {
            width = image.Width;
            height += image.Height;
        }

        var bitmap = new Bitmap(width, height);
        using (var g = Graphics.FromImage(bitmap))
        {
            var localHeight = 0;
            foreach (var image in enumerable)
            {
                g.DrawImage(image, 0, localHeight);
                localHeight += image.Height;
            }
        }
        return bitmap;
    }


    public object ExecuteChromeCommandWithResult(string commandName, Dictionary<string, object> commandParameters)
    {
        if (commandName == null)
        {
            throw new ArgumentNullException("commandName", "commandName must not be null");
        }

        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["cmd"] = commandName;
        parameters["params"] = commandParameters;
        Response response = this.Execute(SendChromeCommandWithResult, parameters);
        return response.Value;
    }

    private Dictionary<string, object> EvaluateDevToolsScript(string scriptToEvaluate)
    {
        // This code is predicated on knowing the structure of the returned
        // object as the result. In this case, we know that the object returned
        // has a "result" property which contains the actual value of the evaluated
        // script, and we expect the value of that "result" property to be an object
        // with a "value" property. Moreover, we are assuming the result will be
        // an "object" type (which translates to a C# Dictionary<string, object>).
        Dictionary<string, object> parameters = new Dictionary<string, object>();
        parameters["returnByValue"] = true;
        parameters["expression"] = scriptToEvaluate;
        object evaluateResultObject = this.ExecuteChromeCommandWithResult("Runtime.evaluate", parameters);
        Dictionary<string, object> evaluateResultDictionary = evaluateResultObject as Dictionary<string, object>;
        Dictionary<string, object> evaluateResult = evaluateResultDictionary["result"] as Dictionary<string, object>;

        // If we wanted to make this actually robust, we'd check the "type" property
        // of the result object before blindly casting to a dictionary.
        Dictionary<string, object> evaluateValue = evaluateResult["value"] as Dictionary<string, object>;
        return evaluateValue;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM