How to take Retina Screenshots with Xvfb and Selenium
Asked Answered
B

3

9

I want to take some screenshots of my hybrid app for itunes connect automatically. I am running Ubuntu 14.04. chromedriver 2.15.322448

Taking screenshots automatically is easy with Selenium and Xvfb. But it is not easy to get retina screenshots.

I started my Xvfb with a higher dpi:

/usr/bin/Xvfb :99 -screen 0 2000x2000x24 -dpi 200

When I check the display information everything seems to be right:

xdpyinfo -display :99

...
screen #0:
  dimensions:    2000x2000 pixels (254x254 millimeters)
  resolution:    200x200 dots per inch
  depths (6):    24, 1, 4, 8, 16, 32
...

Then I start my chromedriver like this

private WebDriver getChromeDriver ( Phone phone )
{
    Map<String, Object> deviceMetrics = new HashMap<String, Object>();
    deviceMetrics.put("width", 320);
    deviceMetrics.put("height", 460);
    deviceMetrics.put("pixelRatio", 2);
    Map<String, Object> mobileEmulation = new HashMap<String, Object>();
    mobileEmulation.put("deviceMetrics", deviceMetrics);
    mobileEmulation.put("userAgent", "iphone4");

    ChromeDriverService cds = new ChromeDriverService.Builder().withEnvironment(ImmutableMap.of("DISPLAY", ":99")).build();

    Map<String, Object> chromeOptions = new HashMap<String, Object>();
    chromeOptions.put("mobileEmulation", mobileEmulation);
    DesiredCapabilities capabilities = DesiredCapabilities.chrome();
    capabilities.setCapability(ChromeOptions.CAPABILITY, chromeOptions);
    WebDriver driver = new ChromeDriver(cds, capabilities);
    return driver;
}

and after some other boring code, I take the screenshot:

 File srcFile = ( (TakesScreenshot) driver ).getScreenshotAs(OutputType.FILE);

This does not work. The screenshot is in regular dpi. So the image of the website captured is only 320x460 and not 640x960 as it should be.

I set a breakpoint just before the Screenshot was taken and dumped the framebuffer like this:

export DISPLAY=:99 
xwd -root -silent | xwdtopnm |pnmtojpeg > screen.jpg

Result of xwd dumping the content of the virtual framebuffer

As you can see the title bar is rendered in respect to higher dpi but the rest of the browser window does not.

So how can I run a chromedriver with more dpi to take retina screenshots? Is it possible?

Bucentaur answered 14/7, 2015 at 7:46 Comment(2)
have you found a solution to this? I'm stuck at exactly the same problem, would appreciate it if you shared your experience with xvfb and retina resolution screenshotsUpstate
I posted an answer below. I hope it helps.Bucentaur
B
6

If you just want to take some screenhosts you can use google chrome headless tool. For example getting a retina screenshot is as easy as

$ google-chrome --headless --hide-scrollbars --disable-gpu \
                --screenshot --force-device-scale-factor=2 \
                --window-size=750,1334 https://www.kicktipp.de/
Bucentaur answered 2/11, 2017 at 18:36 Comment(1)
the --force-device-scale-factor=2 fixed this for me, thank you much!Neighborhood
M
1

I'm facing the same problem and still stuck but the following may be useful. It allowed me to rule out either xvfb or chrome by attaching a VNC connection to the xvfb framebuffer.

#!/bin/bash
export GEOMETRY="$SCREEN_WIDTH""x""$SCREEN_HEIGHT""x""$SCREEN_DEPTH"

function shutdown {
  kill -s SIGTERM $NODE_PID
  wait $NODE_PID
}

sudo -E -i -u seluser \
  DISPLAY=$DISPLAY \
  xvfb-run --server-args="$DISPLAY -screen 0 $GEOMETRY -dpi 300 -ac +extension RANDR" \
  java -jar /opt/selenium/selenium-server-standalone.jar &
NODE_PID=$!

trap shutdown SIGTERM SIGINT
for i in $(seq 1 10)
do
  xdpyinfo -display $DISPLAY >/dev/null 2>&1
  if [ $? -eq 0 ]; then
    break
  fi
  echo Waiting xvfb...
  sleep 0.5
done

fluxbox -display $DISPLAY &

x11vnc -forever -usepw -shared -rfbport 5900 -display $DISPLAY &

wait $NODE_PID

After VNC'ing in, google-chrome GUI can be loaded from the terminal. Navigation to web pages confirm that Chrome is rendering the pages with the correct DPI. Screenshot https://i.sstatic.net/iEjo0.jpg

I would really like to get this working too so please reach out if you have any new developments. I used https://registry.hub.docker.com/u/selenium/standalone-chrome-debug/ BTW.

Mistake answered 15/7, 2015 at 0:3 Comment(0)
B
0

I switched to Firefox and it worked for me with the following code. But at the moment it doesn't as selenium is not working fine with my Firefox Version 47, see https://github.com/SeleniumHQ/selenium/issues/2257 So I can't test this code right now, but last time I was able to get retina screenshots with it:

package de.kicktipp.screenshots.stackoverflow;

import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import javax.imageio.ImageIO;

import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxBinary;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;

public class ScreenshotMaker
{
    private PhoneList           phoneList   = new PhoneList();

    private static final String HOST        = "https://m.kicktipp.de";
    private static final String PATH        = "/";

    private File                resultDirectory;
    private int                 filenumber  = 0;

    public static void main ( String[] args ) throws Exception
    {
        ScreenshotMaker screenshotMaker = new ScreenshotMaker();
        screenshotMaker.run();
    }

    public WebDriver getDriver ( Phone phone, Display display )
    {
        FirefoxProfile profile = new FirefoxProfile();
        // profile.setPreference("layout.css.devPixelsPerPx", "2.0");
        // Ansonsten erscheint ein hässliches Popup welches Reader Funktion
        // anbietet
        profile.setPreference("reader.parse-on-load.enabled", false);
        profile.setPreference("xpinstall.signatures.required", false);
        FirefoxBinary firefoxBinary = new FirefoxBinary();
        firefoxBinary.setEnvironmentProperty("DISPLAY", display.getDisplayNumberString());
        FirefoxDriver firefoxDriver = new FirefoxDriver(firefoxBinary, profile);
        firefoxDriver.manage().window().setSize(new Dimension(phone.getWidth(), display.getHeight()));
        return firefoxDriver;
    }

    private void run ( ) throws Exception
    {
        mkdir();
        for (Phone phone : phoneList)
        {
            WebDriver driver = null;
            Display display = null;
            try
            {
                display = new Display(phone.getDpiFaktor());
                driver = getDriver(phone, display);
                System.out.println(phone.getName());
                filenumber = 0;
                System.out.println("");
                System.out.println("Generating Screenshots for " + phone.getName());
                System.out.println("-----------------------------------------------------------------------------");
                driver.get(HOST + "/");
                shot(display, driver, PATH, phone);
            }
            finally
            {
                if (driver != null)
                {
                    driver.quit();
                }
                if (display != null)
                {
                    display.shutdown();
                }
            }
        }
        System.out.println("");
        System.out.println("-----------------------------------------------------------------------------");
        System.out.println("Finished.");

    }

    private void mkdir ( ) throws IOException
    {
        File targetDir = targetDir();
        resultDirectory = new File(targetDir, "results");
        resultDirectory.mkdir();
        System.out.println("Writing screenshots to " + resultDirectory.getCanonicalPath());
    }

    public File targetDir ( )
    {
        String relPath = getClass().getProtectionDomain().getCodeSource().getLocation().getFile();
        File targetDir = new File(relPath + "../..");
        if (!targetDir.exists())
        {
            targetDir.mkdir();
        }
        return targetDir;
    }

    private void shot ( Display display, WebDriver driver, String path, Phone phoneSpec ) throws Exception
    {
        String url = getUrl(path);
        driver.get(url);
        scrollToRemoveScrollbars(driver);
        // Selenium screenshot doesn't work, we are dumping the framebuffer
        // directly
        File srcFile = display.captureScreenshot();
        moveFile(srcFile, phoneSpec);
    }

    private void scrollToRemoveScrollbars ( WebDriver driver ) throws Exception
    {
        JavascriptExecutor js = (JavascriptExecutor) driver;
        js.executeScript("window.scrollTo(0,20);");
        js.executeScript("window.scrollTo(0,0);");
        Thread.sleep(800);
    }

    private String getUrl ( String path )
    {
        StringBuffer url = new StringBuffer(HOST);
        url.append(path);
        return url.toString();
    }

    private void moveFile ( File srcFile, Phone phone ) throws Exception
    {
        String filename = phone.getFilename(filenumber);
        File file = new File(resultDirectory, filename);
        if (file.exists())
        {
            file.delete();
        }
        crop(srcFile, file, phone);
        System.out.println(filename);
    }

    private void crop ( File srcFile, File targetFile, Phone phone ) throws Exception
    {
        int width = phone.getPixelWidth();
        int height = phone.getPixelHeight();
        int yStart = 71 * phone.getDpiFaktor();
        Image orig = ImageIO.read(srcFile);
        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        bi.getGraphics().drawImage(orig, 0, 0, width, height, 0, yStart, width, height + yStart, null);
        ImageIO.write(bi, "png", targetFile);
    }
}

class PhoneList extends ArrayList<Phone>
{
    private static final Phone  IPHONE_4        = new Phone(320, 460, "iphone4", 2);
    private static final Phone  IPHONE_5        = new Phone(320, 548, "iphone5", 2);
    private static final Phone  IPHONE_6        = new Phone(375, 667, "iphone6", 2);
    private static final Phone  IPAD            = new Phone(1024, 748, "ipad", 2);
    private static final Phone  IPHONE_6_PLUS   = new Phone(414, 736, "iphone6plus", 3);
    private static final Phone  AMAZON          = new Phone(480, 800, "amazon", 1);

    public PhoneList ()
    {
        add(AMAZON);
        add(IPHONE_4);
        add(IPHONE_5);
        add(IPHONE_6);
        add(IPAD);
        add(IPHONE_6_PLUS);
    }
}

class Phone
{
    private int     width       = 0;
    private int     height      = 0;
    private String  name        = "";
    private int     dpiFaktor   = 2;

    public Phone ( int width, int height, String name, int dpiFaktor )
    {
        this.width = width;
        this.height = height;
        this.name = name;
        this.dpiFaktor = dpiFaktor;
    }

    public int getWidth ( )
    {
        return width;
    }

    public int getHeight ( )
    {
        return height;
    }

    public int getPixelWidth ( )
    {
        return width * dpiFaktor;
    }

    public int getPixelHeight ( )
    {
        return height * dpiFaktor;
    }

    public int getDpiFaktor ( )
    {
        return dpiFaktor;
    }

    public String getName ( )
    {
        return name;
    }

    public Dimension getDimension ( )
    {
        return new Dimension(width, height);
    }

    public String getFilename ( int number )
    {
        String dimension = getPixelWidth() + "x" + getPixelHeight();
        return name + "-" + dimension + "-" + number + ".png";
    }
}

class Display
{
    private static final int    HEIGHT                  = 5000;
    private static final int    WIDTH                   = 5000;
    private static String       XVFB                    = "/usr/bin/Xvfb";
    private static String       DISPLAY_NUMBER_STRING   = ":99";
    private static String       SCREEN_SIZE             = " -screen 0 " + WIDTH + "x" + HEIGHT + "x24";
    private static String       XVFB_COMMAND            = XVFB + " " + DISPLAY_NUMBER_STRING + SCREEN_SIZE + " -dpi ";
    private static int          baseDpi                 = 100;
    private Process             p;

    public Display ( int dpiFaktor ) throws IOException, InterruptedException
    {
        checkExecutable();
        int dpi = baseDpi * dpiFaktor;
        String cmd = XVFB_COMMAND + dpi;
        p = Runtime.getRuntime().exec(cmd);
        Thread.sleep(1000);
        try
        {
            int exitValue = p.exitValue();
            String msgTemplate = "ERROR: Exit Value: %s. Display konnte nicht gestartet werden. Läuft ein Display noch auf %s ?";
            String msg = String.format(msgTemplate, exitValue, DISPLAY_NUMBER_STRING);
            throw new IllegalStateException(msg);
        }
        catch (IllegalThreadStateException e)
        {
            // Das ist gut, der Prozess ist noch nicht beendet.
            System.out.println("Switched on display at " + dpi + "dpi with command " + cmd);
            return;
        }
    }

    private void checkExecutable ( )
    {
        File file = new File(XVFB);
        if (!file.canExecute())
        {
            System.err.println("Xvfb is not installed at " + XVFB);
            System.err.println("Install Xvfb by runing");
            System.err.println("apt-get install xvfb");
        }
    }

    public File captureScreenshot ( ) throws IOException, InterruptedException
    {
        File tempFile = File.createTempFile("screenshots", ".png");
        String absolutePath = tempFile.getAbsolutePath();
        String cmd = "import -window root " + absolutePath;
        String[] env = new String[] { "DISPLAY=" + DISPLAY_NUMBER_STRING };
        Process exec = Runtime.getRuntime().exec(cmd, env);
        exec.waitFor();
        return tempFile;
    }

    public void shutdown ( ) throws IOException, InterruptedException
    {
        p.destroy();
        try
        {
            Thread.sleep(1000);
            int exitValue = p.exitValue();
            System.out.println("Display was switched off. ExitValue: " + exitValue);
        }
        catch (IllegalThreadStateException e)
        {
            // Das ist nicht gut, der Prozess sollte beendet sein.
            // Kill it:
            p = Runtime.getRuntime().exec("pkill Xvfb");
        }
    }

    public String getDisplayNumberString ( )
    {
        return DISPLAY_NUMBER_STRING;
    }

    public int getHeight ( )
    {
        return HEIGHT;
    }
}

And this is my pom:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>de.kicktipp</groupId>
    <artifactId>screenshots</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>screenshots</name>
    <properties>
        <jdk.version>1.7</jdk.version>
        <maven.version>3.0</maven.version>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <selenium-java.version>2.53.1</selenium-java.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${jdk.version}</source>
                    <target>${jdk.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium-java.version}</version>
        </dependency>
    </dependencies>
</project>
Bucentaur answered 4/7, 2016 at 13:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.