1

I want to render some custom-made CSS3/Javascript animated sequences to sets of PNG files on server side to join them into a single video file next.

I saw here it was possible with PhantomJS. As I don't have a big background with Selenium, I don't know how to adapt it with Selenium. The only thing I know is how to make a single screenshot with Selenium :

driver = webdriver.Chrome()
driver.get('mywebpage.com')
driver.save_screenshot('out.png')
driver.quit()

But it only perform a single screenshot.

Please how to take a set of screenshots from the beginning to end of a CSS/Javascript animation through Selenium/Python.

PS: I use Python 3.5 and chrome as selenium webdriver on a Vagrant VM

Many thanks in advance

Community
  • 1
  • 1
kabrice
  • 1,475
  • 6
  • 27
  • 51

3 Answers3

1

What you try to archive is not possible in Selenium with ease.

The only way to archive this would be to get control over the animation frame. One possible way is described here.

Another way would be to access directly the javascript methods who are setting the CSS properties.

So in general, there's no native support.

Arakis
  • 849
  • 5
  • 16
1

EDIT4:

As per @Arakis pointed out, what you want (enough screenshots about any given animation to worth making a video) is not possible without taking over the animation in the browser. If you are not able to take over the animation, this is the best you can get:

  1. load up the page
  2. while the animation lasts, create as many screenshots as possible
  3. save the data
import os
import time

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

TARGET_FOLDER = r'C:\test\animated_you_not_wanted\{}.png'
WINDOW_SIZE = 1920, 1080

options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("start-maximized")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")

shots = []
if not os.path.isdir(os.path.dirname(TARGET_FOLDER)):
    os.makedirs(os.path.dirname(TARGET_FOLDER))

driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe', options=options,
                          service_args=['--ignore-ssl-errors=true', '--ssl-protocol=any'])
# driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe')
driver.set_window_size(*WINDOW_SIZE)
driver.get('https://daneden.github.io/animate.css/')


# we know that the animation duration on this page is 1 sec so we will try to record as many frames as possible in 1 sec
start_time = time.time()
while time.time() <= start_time + 1:
    shots.append(driver.get_screenshot_as_png())

# dumping all captured frames
for i in range(len(shots)):
    with open(TARGET_FOLDER.format(i), "wb") as f:
        f.write(shots[i])

If you run this code above, you will get approx 3 screenshots, maybe 4 if you have a beast machine.

Why is this happening? When selenium grabs a screenshot, the animation in the browser won't stop and wonder if is it feasible to move on, it just runs independent as normal. Selenium in this resolution (1920x1080) is capable of grabbing 3-4 screenshots in a second. If you reduce the screen resolution to 640x480, you'll get 5-7 screenshots per second depending on your machine, but that frame rate is still very-very far form what you probably desire. Selenium interacts with a browser trough an API but does not control the browser. Taking a screenshot takes time and while selenium grabs the rendered page as an image, animation moves on.

If you want to have a smooth animation with high frame rate, you have to take control over the animation by overriding the animation states. You have to:

  • set the animation-play-state to paused
  • set the animation-duration to a reasonable length
  • set the animation-delay to a negative value to select a given animation state
  • in a for loop adjust the animation-delay and grab a screenshot
  • save screenshot data later

On the given page (https://daneden.github.io/animate.css/) in the opening animation there are 2 webelements animated with the same animation. So what the code below does is the list above with the little extra: it 'steps' the animation cycle for both elements.

import os
import time

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

TARGET_FOLDER = r'C:\test\animated_you_wanted\{}.png'
WINDOW_SIZE = 1920, 1080
ANIM_DURATION = 2
FRAMES = 60
BASE_SCR = 'arguments[0].setAttribute("style", "display: block;animation-delay: {}s;animation-duration: {}s; animation-iteration-count: infinite;animation-play-state: paused;")'

options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("start-maximized")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")

driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe', options=options,
                          service_args=['--ignore-ssl-errors=true', '--ssl-protocol=any'])
# driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe')
driver.set_window_size(*WINDOW_SIZE)
driver.get('https://daneden.github.io/animate.css/')

header = driver.find_element_by_xpath('//*[@class="site__header island"]')
content = driver.find_element_by_xpath('//*[@class="site__content island"]')

shots = []
for frame in range(FRAMES):
    for elem in (header, content):
        driver.execute_script(BASE_SCR.format((frame / FRAMES) * -ANIM_DURATION, ANIM_DURATION), elem)
    shots.append(driver.get_screenshot_as_png())

if not os.path.isdir(os.path.dirname(TARGET_FOLDER)):
    os.makedirs(os.path.dirname(TARGET_FOLDER))

# dumping all captured frames
for i in range(len(shots)):
    with open(TARGET_FOLDER.format(i), "wb") as f:
        f.write(shots[i])

This is the best you can get with selenium.

BONUS: (or edit5?)

I was wondering if is really impossible to squeeze out more than 5-6 frames per second with a completely generic solution (webpage-independent). I was thinking on this:

  • there are JS libraries which are capable of converting html elements into images
  • it is possible to inject JS to the page with selenium
  • since there are no API calls , a pure JS screenshot logic could outperform the pure selenium approach

So I decided to:

  • inject the JS library to the page
  • reload the document.body to reset the animation
  • grab the screenshots and save them

This is the code, contains more JS than python :D :

import base64
import os
import time

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

TARGET_FOLDER = r'C:\test\animated_you_actually_wanted\{}.png'
WINDOW_SIZE = 800, 600
ANIM_DURATION = 1
FRAMES = 15
BASE_SCR = """
function load_script(){
    let ss = document.createElement("script");
    ss.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js";
    ss.type = "text/javascript";
    document.getElementsByTagName("head")[0].appendChild(ss);
};

load_script();
shots = [];

window.take = function() {
  html2canvas(document.body, {
    onrendered: function (canvas) {
      shots.push(canvas);
    }
  })
};

window.record = function(times, sleep){
    for (let i=0; i<times; i++){
        setTimeout(window.take(), sleep*i)
        console.log("issued screenshot with sleep: " + sleep*i)
    }
};
""" + """ 
document.body.setAttribute("style", "width: {width}px; height: {height}px");
""".format(width=WINDOW_SIZE[1], height=WINDOW_SIZE[0])


RECORD_SCR = """
document.body.innerHTML = document.body.innerHTML
window.record({}, {})
"""

GRAB_SCR = """

function getShots(){
    let retval = []
    for (let i=0; i<shots.length; i++){
        retval.push(shots[i].toDataURL("image/png"))
    }
    return retval;
}

return getShots()
"""

options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("start-maximized")
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")

if not os.path.isdir(os.path.dirname(TARGET_FOLDER)):
    os.makedirs(os.path.dirname(TARGET_FOLDER))

driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe', options=options,
                          service_args=['--ignore-ssl-errors=true', '--ssl-protocol=any'])
# driver = webdriver.Chrome(executable_path=r'c:\_\chromedriver.exe')
driver.set_window_size(*WINDOW_SIZE)
driver.get('https://daneden.github.io/animate.css/')

driver.execute_script(BASE_SCR)
time.sleep(3)
driver.execute_script(RECORD_SCR.format(FRAMES, (ANIM_DURATION/FRAMES)*100))

shots = []
while len(shots) < FRAMES:
    shots = driver.execute_script(GRAB_SCR)


# dumping all captured frames
for i in range(len(shots)):
    with open(TARGET_FOLDER.format(i), "wb") as f:
        f.write(base64.urlsafe_b64decode(shots[i].split('base64,')[1]))

driver.quit()

Outcome of the html2canvas experiment:

  • the sweet-spot regarding frame rate is around 15-ish on the page mentioned above. More frames cause bottlenecks, especially in the first few frames where the text float-in initial position increases the document.body size
  • target element (document.body) dimension heavily impacts the performance
  • bottlenecks everywhere. I've tried to avoid magic numbers, wasn't easy
  • rendered image may contain glitches, like texts outside their container
  • it may be worth trying a different library, others may perform better
  • this result is still lightyears away from the 60fps frame rate one can achieve with a non-generic solution
  • one can get more frames in headless mode
Trapli
  • 1,517
  • 2
  • 13
  • 19
-1

First of all: export it into separate files:

driver = webdriver.Chrome()
driver.get('mywebpage.com')
for count in range(1, (number_of_frames + 1)):
    driver.save_screenshot('out_{}.png'.format(count))
driver.quit()
pkolawa
  • 653
  • 6
  • 17
  • Thank you for the quick answer. But sadly, it doesn't work properly, it's like it takes snapchot each second, because I have no differences on generated images when capturing animations of this webpage: https://daneden.github.io/animate.css/ (I have set 10 frames, and the animation last less than 1 second). Please is there a way to set time frequency for when the snapshot will be taken starting when the page is loading for the first time? Is there also a way to detect when the animation will finish in order to adjust the iterator (or number_of_frames ) ? Thx – kabrice Oct 11 '18 at 12:38