Take full page screenshot in Chrome with Selenium

前端 未结 2 2052
清歌不尽
清歌不尽 2020-11-27 15:21

I know this was not possible before, but now with the following update:

https://developers.google.com/web/updates/2017/04/devtools-release-notes#screenshots

2条回答
  •  Happy的楠姐
    2020-11-27 15:44

    To do this with Selenium Webdriver in Java takes a bit of work.. As hinted by Florent B. we need to change some classes uses by the default ChromeDriver to make this work. First we need to make a new DriverCommandExecutor which adds the new Chrome commands:

    import com.google.common.collect.ImmutableMap;
    import org.openqa.selenium.remote.CommandInfo;
    import org.openqa.selenium.remote.http.HttpMethod;
    import org.openqa.selenium.remote.service.DriverCommandExecutor;
    import org.openqa.selenium.remote.service.DriverService;
    
    public class MyChromeDriverCommandExecutor extends DriverCommandExecutor {
        private static final ImmutableMap CHROME_COMMAND_NAME_TO_URL;
    
        public MyChromeDriverCommandExecutor(DriverService service) {
            super(service, CHROME_COMMAND_NAME_TO_URL);
        }
    
        static {
            CHROME_COMMAND_NAME_TO_URL = ImmutableMap.of("launchApp", new CommandInfo("/session/:sessionId/chromium/launch_app", HttpMethod.POST)
            , "sendCommandWithResult", new CommandInfo("/session/:sessionId/chromium/send_command_and_get_result", HttpMethod.POST)
            );
        }
    }
    

    After that we need to create a new ChromeDriver class which will then use this thing. We need to create the class because the original has no constructor that lets us replace the command executor... So the new class becomes:

    import com.google.common.collect.ImmutableMap;
    import org.openqa.selenium.Capabilities;
    import org.openqa.selenium.WebDriverException;
    import org.openqa.selenium.chrome.ChromeDriverService;
    import org.openqa.selenium.html5.LocalStorage;
    import org.openqa.selenium.html5.Location;
    import org.openqa.selenium.html5.LocationContext;
    import org.openqa.selenium.html5.SessionStorage;
    import org.openqa.selenium.html5.WebStorage;
    import org.openqa.selenium.interactions.HasTouchScreen;
    import org.openqa.selenium.interactions.TouchScreen;
    import org.openqa.selenium.mobile.NetworkConnection;
    import org.openqa.selenium.remote.FileDetector;
    import org.openqa.selenium.remote.RemoteTouchScreen;
    import org.openqa.selenium.remote.RemoteWebDriver;
    import org.openqa.selenium.remote.html5.RemoteLocationContext;
    import org.openqa.selenium.remote.html5.RemoteWebStorage;
    import org.openqa.selenium.remote.mobile.RemoteNetworkConnection;
    
    public class MyChromeDriver  extends RemoteWebDriver implements LocationContext, WebStorage, HasTouchScreen, NetworkConnection {
        private RemoteLocationContext locationContext;
        private RemoteWebStorage webStorage;
        private TouchScreen touchScreen;
        private RemoteNetworkConnection networkConnection;
    
        //public MyChromeDriver() {
        //  this(ChromeDriverService.createDefaultService(), new ChromeOptions());
        //}
        //
        //public MyChromeDriver(ChromeDriverService service) {
        //  this(service, new ChromeOptions());
        //}
    
        public MyChromeDriver(Capabilities capabilities) {
            this(ChromeDriverService.createDefaultService(), capabilities);
        }
    
        //public MyChromeDriver(ChromeOptions options) {
        //  this(ChromeDriverService.createDefaultService(), options);
        //}
    
        public MyChromeDriver(ChromeDriverService service, Capabilities capabilities) {
            super(new MyChromeDriverCommandExecutor(service), capabilities);
            this.locationContext = new RemoteLocationContext(this.getExecuteMethod());
            this.webStorage = new RemoteWebStorage(this.getExecuteMethod());
            this.touchScreen = new RemoteTouchScreen(this.getExecuteMethod());
            this.networkConnection = new RemoteNetworkConnection(this.getExecuteMethod());
        }
    
        @Override
        public void setFileDetector(FileDetector detector) {
            throw new WebDriverException("Setting the file detector only works on remote webdriver instances obtained via RemoteWebDriver");
        }
    
        @Override
        public LocalStorage getLocalStorage() {
            return this.webStorage.getLocalStorage();
        }
    
        @Override
        public SessionStorage getSessionStorage() {
            return this.webStorage.getSessionStorage();
        }
    
        @Override
        public Location location() {
            return this.locationContext.location();
        }
    
        @Override
        public void setLocation(Location location) {
            this.locationContext.setLocation(location);
        }
    
        @Override
        public TouchScreen getTouch() {
            return this.touchScreen;
        }
    
        @Override
        public ConnectionType getNetworkConnection() {
            return this.networkConnection.getNetworkConnection();
        }
    
        @Override
        public ConnectionType setNetworkConnection(ConnectionType type) {
            return this.networkConnection.setNetworkConnection(type);
        }
    
        public void launchApp(String id) {
            this.execute("launchApp", ImmutableMap.of("id", id));
        }
    }
    

    This is mostly a copy of the original class, but with some constructors disabled (because some of the needed code is package private). If you are in need of these constructors you must place the classes in the package org.openqa.selenium.chrome.

    With these changes you are able to call the required code, as shown by Florent B., but now in Java with the Selenium API:

    import com.google.common.collect.ImmutableMap;
    import org.openqa.selenium.remote.Command;
    import org.openqa.selenium.remote.Response;
    
    import javax.annotation.Nonnull;
    import javax.annotation.Nullable;
    import javax.imageio.ImageIO;
    import java.awt.image.BufferedImage;
    import java.io.ByteArrayInputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ChromeExtender {
        @Nonnull
        private MyChromeDriver m_wd;
    
        public ChromeExtender(@Nonnull MyChromeDriver wd) {
            m_wd = wd;
        }
    
        public void takeScreenshot(@Nonnull File output) throws Exception {
            Object visibleSize = evaluate("({x:0,y:0,width:window.innerWidth,height:window.innerHeight})");
            Long visibleW = jsonValue(visibleSize, "result.value.width", Long.class);
            Long visibleH = jsonValue(visibleSize, "result.value.height", Long.class);
    
            Object contentSize = send("Page.getLayoutMetrics", new HashMap<>());
            Long cw = jsonValue(contentSize, "contentSize.width", Long.class);
            Long ch = jsonValue(contentSize, "contentSize.height", Long.class);
    
            /*
             * In chrome 61, delivered one day after I wrote this comment, the method forceViewport was removed.
             * I commented it out here with the if(false), and hopefully wrote a working alternative in the else 8-/
             */
            if(false) {
                send("Emulation.setVisibleSize", ImmutableMap.of("width", cw, "height", ch));
                send("Emulation.forceViewport", ImmutableMap.of("x", Long.valueOf(0), "y", Long.valueOf(0), "scale", Long.valueOf(1)));
            } else {
                send("Emulation.setDeviceMetricsOverride",
                    ImmutableMap.of("width", cw, "height", ch, "deviceScaleFactor", Long.valueOf(1), "mobile", Boolean.FALSE, "fitWindow", Boolean.FALSE)
                );
                send("Emulation.setVisibleSize", ImmutableMap.of("width", cw, "height", ch));
            }
    
            Object value = send("Page.captureScreenshot", ImmutableMap.of("format", "png", "fromSurface", Boolean.TRUE));
    
            // Since chrome 61 this call has disappeared too; it does not seem to be necessary anymore with the new code.
            // send("Emulation.resetViewport", ImmutableMap.of()); 
            send("Emulation.setVisibleSize", ImmutableMap.of("x", Long.valueOf(0), "y", Long.valueOf(0), "width", visibleW, "height", visibleH));
    
            String image = jsonValue(value, "data", String.class);
            byte[] bytes = Base64.getDecoder().decode(image);
    
            try(FileOutputStream fos = new FileOutputStream(output)) {
                fos.write(bytes);
            }
        }
    
        @Nonnull
        private Object evaluate(@Nonnull String script) throws IOException {
            Map param = new HashMap<>();
            param.put("returnByValue", Boolean.TRUE);
            param.put("expression", script);
    
            return send("Runtime.evaluate", param);
        }
    
        @Nonnull
        private Object send(@Nonnull String cmd, @Nonnull Map params) throws IOException {
            Map exe = ImmutableMap.of("cmd", cmd, "params", params);
            Command xc = new Command(m_wd.getSessionId(), "sendCommandWithResult", exe);
            Response response = m_wd.getCommandExecutor().execute(xc);
    
            Object value = response.getValue();
            if(response.getStatus() == null || response.getStatus().intValue() != 0) {
                //System.out.println("resp: " + response);
                throw new MyChromeDriverException("Command '" + cmd + "' failed: " + value);
            }
            if(null == value)
                throw new MyChromeDriverException("Null response value to command '" + cmd + "'");
            //System.out.println("resp: " + value);
            return value;
        }
    
        @Nullable
        static private  T jsonValue(@Nonnull Object map, @Nonnull String path, @Nonnull Class type) {
            String[] segs = path.split("\\.");
            Object current = map;
            for(String name: segs) {
                Map cm = (Map) current;
                Object o = cm.get(name);
                if(null == o)
                    return null;
                current = o;
            }
            return (T) current;
        }
    }
    

    This lets you use the commands as specified, and creates a file with a png format image inside it. You can of course also directly create a BufferedImage by using ImageIO.read() on the bytes.

提交回复
热议问题