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

How to take Retina Screenshots with Xvfb and Selenium

我想为我的 iTunes 自动连接混合应用截取一些屏幕截图。我是 运行宁 Ubuntu 14.04。 chromedriver 2.15.322448

使用 Selenium 和 Xvfb 可以轻松自动截取屏幕截图。但是要获取retina截图并不容易

我以更高的 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

如您所见,标题栏是根据更高的 dpi 呈现的,但浏览器的其余部分 window 不会。

那么我如何 运行 具有更高 dpi 的 chromedriver 来拍摄 Retina 屏幕截图?可能吗?

我遇到了同样的问题,但仍然卡住了,但以下内容可能会有用。它允许我通过将 VNC 连接附加到 xvfb 帧缓冲区来排除 xvfb 或 chrome。

#!/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

进入 VNC 后,google-chrome 可以从终端加载 GUI。导航到网页确认 Chrome 正在使用正确的 DPI 呈现页面。截图 http://i.stack.imgur.com/iEjo0.jpg

我真的很想让它也能正常工作,所以如果您有任何新进展,请与我们联系。我用了 https://registry.hub.docker.com/u/selenium/standalone-chrome-debug/ 顺便说一句。

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

package de.kicktipp.screenshots.Whosebug;

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>

如果您只想获取一些屏幕主机,您可以使用 google chrome headless 工具。例如获取视网膜屏幕截图就像

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