如何使用Xvfb和Selenium拍摄Retina屏幕截图

时间:2015-07-14 07:46:13

标签: selenium screenshot retina-display selenium-chromedriver xvfb

我想为我的混合应用程序拍摄一些截图,以便自动连接itunes。我正在运行Ubuntu 14.04。 chromedriver 2.15.322448

使用Selenium和Xvfb可以轻松截取屏幕截图。但是获取视网膜截图并不容易。

我用更高的dpi开始了我的Xvfb:

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

当我检查显示信息时,一切似乎都是正确的:

xdpyinfo -display :99

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

然后我就像这样开始我的chromedriver

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;
}

在其他一些无聊的代码之后,我会截取屏幕截图:

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

这不起作用。屏幕截图是常规dpi。因此,所捕获网站的图像仅为320x460而不是640x960。

我在截取屏幕截图之前设置了断点,然后像这样转储帧缓冲区:

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

Result of xwd dumping the content of the virtual framebuffer

正如您所看到的,标题栏是针对较高的dpi呈现的,但浏览器窗口的其余部分则没有。

那么我怎样才能运行一个带有更多dpi的chromedriver来拍摄视网膜截图?有可能吗?

3 个答案:

答案 0 :(得分:3)

如果您只想拍摄一些屏幕主机,可以使用Google Chrome无头工具。例如,获取视网膜截图就像

一样简单
$ google-chrome --headless --hide-scrollbars --disable-gpu \
                --screenshot --force-device-scale-factor=2 \
                --window-size=750,1334 https://www.kicktipp.de/

答案 1 :(得分:1)

我面临同样的问题并且仍然存在,但以下内容可能会有用。它允许我通过将一个VNC连接附加到xvfb帧缓冲区来排除xvfb或chrome。

log:requests

在VNC进入后,可以从终端加载#!/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 GUI。导航到网页确认Chrome正在使用正确的DPI呈现页面。屏幕截图http://i.stack.imgur.com/iEjo0.jpg

我真的很想让这个也工作,所以如果你有任何新的发展,请联系我们。我使用了https://registry.hub.docker.com/u/selenium/standalone-chrome-debug/ BTW。

答案 2 :(得分:0)

我切换到Firefox,它使用以下代码为我工作。但目前它没有因为我的Firefox版本47没有使用selenium,请参阅https://github.com/SeleniumHQ/selenium/issues/2257所以我现在无法测试此代码,但上次我能够使用它获取视网膜截图:

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;
    }
}

这是我的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>