Tuesday Tip Day – Pixel Ratio

Tuesday Tip Day – Pixel Ratio

Pixel Ratio Confusion

This is a tip being written off the back of me struggling with an issue, in the hope that it will save someone the stress I went through.

The context. Taking full screen mobile screenshots on mobile devices via a platform like BrowserStack, where the screenshot is being taken on a real device. On mobile devices, a pixel isn’t a pixel like it is on your desktop. Devices like an Apple iPad Pro, will have a physical resolution of 2732×2048, but a logical resolution of half that, because the pixel ratio of that device is 2. This, in simple terms, means a physical pixel is worth two digital pixels. It’s how the CSS of a site interprets the devices resolution.

This causes many issues when trying to take screenshots on these devices. As an example, if you calculate a viewport width and height based off the JavaScript measurements of inner.height and client.width, it will return the logical resolution sizes, which means if we take a screenshot of the viewport using those sizes, we’ll only get back half the image we’re expecting.

This means we need to both get the pixel ratio of the device, and then recalculate any measurements using that pixel ratio.

In a previous article, I gave you a solution to taking full screen (not just viewport) screenshots of a page for desktop. Let’s now modify that to work with mobile devices as well as desktop:

public Image GetEntireScreenshot(bool isMobile = false)
        {
            var pixelRatio = 1; 
            if (isMobile) pixelRatio = (int) (long) ((IJavaScriptExecutor) driver).ExecuteScript("return window.devicePixelRatio");

            // Size of page
            var totalWidth = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return document.body.offsetWidth") * pixelRatio);
            var totalHeight = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return  document.body.parentNode.scrollHeight") * pixelRatio);

            // Size of the viewport
            var viewportWidth = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return document.body.clientWidth") * pixelRatio);
            var viewportHeight = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return window.innerHeight") * pixelRatio);


            var screenshot = (ITakesScreenshot)driver;
            ((IJavaScriptExecutor)driver).ExecuteScript("window.scrollTo(0, 0)");

            if (totalWidth <= viewportWidth && totalHeight <= viewportHeight) return ScreenshotToImage(screenshot.GetScreenshot());

            var rectangles = new List<Rectangle>();

            // Loop until the totalHeight is reached
            for (var y = 0; y < totalHeight; y += viewportHeight)
            {
                var newHeight = viewportHeight;

                // Fix if the height of the element is too big
                if (y + viewportHeight > totalHeight) newHeight = totalHeight - y;

                // Loop until the totalWidth is reached
                for (var x = 0; x < totalWidth; x += viewportWidth)
                {
                    var newWidth = viewportWidth;
                    // Fix if the Width of the Element is too big
                    if (x + viewportWidth > totalWidth) newWidth = totalWidth - x;

                    // Create and add the Rectangle
                    rectangles.Add(new Rectangle(x, y, newWidth, newHeight));
                }
            }

            var stitchedImage = new Bitmap(totalWidth, totalHeight);
            var previous = Rectangle.Empty;
            foreach (var rectangle in rectangles)
            {
                // Calculate scrolling (if needed)
                if (previous != Rectangle.Empty) ((IJavaScriptExecutor) driver).ExecuteScript($"window.scrollBy({(rectangle.Right - previous.Right) / pixelRatio}, {(rectangle.Bottom - previous.Bottom) / pixelRatio})");

                // Calculate the source Rectangle
                var sourceRectangle = new Rectangle(viewportWidth - rectangle.Width,
                                                    viewportHeight - rectangle.Height,
                                                    rectangle.Width, rectangle.Height);

                // Copy the Image
                using (var graphics = Graphics.FromImage(stitchedImage))
                {
                    graphics.DrawImage(ScreenshotToImage(screenshot.GetScreenshot()), rectangle, sourceRectangle, GraphicsUnit.Pixel);
                }

                previous = rectangle;
            }
            return stitchedImage;
        }

So what have we done to adapt the original code?

if (isMobile) pixelRatio = (int) (long) ((IJavaScriptExecutor) driver).ExecuteScript("return window.devicePixelRatio");

This line will check if we’re running on a mobile device, and if so, execute a piece of JavaScript to find out our device pixel ratio. In the example we used above, of an iPad Pro, this would return an integer of 2.

  // Size of page
            var totalWidth = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return document.body.offsetWidth") * pixelRatio);
            var totalHeight = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return  document.body.parentNode.scrollHeight") * pixelRatio);

            // Size of the viewport
            var viewportWidth = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return document.body.clientWidth") * pixelRatio);
            var viewportHeight = Convert.ToInt32((int)(long)((IJavaScriptExecutor)driver).ExecuteScript("return window.innerHeight") * pixelRatio);

This code is almost identical, but you can see we are now multiplying the values we get back from the JavaScript code by the pixel ratio, to convert the logical size to the physical size. This ensures we are going to be capturing the page in its entirety, and when we split the page into our viewport rectangles, we are also calculating our rectangles at the right size.

if (previous != Rectangle.Empty) ((IJavaScriptExecutor) driver).ExecuteScript($"window.scrollBy({(rectangle.Right - previous.Right) / pixelRatio}, {(rectangle.Bottom - previous.Bottom) / pixelRatio})");

The last change to the code we originally had is the above, where we calculate how much to scroll by. You might be wondering why we are now dividing the value of X and Y by the pixel ratio instead of multiplying it. This is because our scroll values are actually unaffected by the pixel ratio, so if we left them as they were (the values were originally multiplied by the pixel ratio), we would be scrolling far too much and missing parts of our page

There you have it, a fully functioning full page screenshot taking solution that works on both mobile and desktop, and you’ve saved yourself the stress that I went through working all this out. I hope this helps you and saves you some time and bother, but if you have any issues or questions with it, please do get in touch and I’m happy to help.

One Response

Comments are closed.