Tightened up the code wrapping the Selenium webdriver.

master
Pacman Ghost 5 years ago
parent 0a4d40abfd
commit 3dc9a61251
  1. 75
      vasl_templates/webapp/utils.py
  2. 60
      vasl_templates/webapp/vassal.py
  3. 126
      vasl_templates/webapp/webdriver.py

@ -4,11 +4,6 @@ import os
import tempfile
import pathlib
from selenium import webdriver
from PIL import Image, ImageChops
from vasl_templates.webapp import app
# ---------------------------------------------------------------------
class TempFile:
@ -41,76 +36,6 @@ class TempFile:
# ---------------------------------------------------------------------
class HtmlScreenshots:
"""Generate preview screenshots of HTML."""
def __init__( self ):
self.webdriver = None
def __enter__( self ):
"""Initialize the HTML screenshot engine."""
webdriver_path = app.config.get( "WEBDRIVER_PATH" )
if not webdriver_path:
raise SimpleError( "No webdriver has been configured." )
# NOTE: If we are being run on Windows without a console (e.g. the frozen PyQt desktop app),
# Selenium will launch the webdriver in a visible DOS box :-( There's no way to turn this off,
# but it can be disabled by modifying the Selenium source code. Find the subprocess.Popen() call
# in $/site-packages/selenium/webdriver/common/service.py and add the following parameter:
# creationflags = 0x8000000 # win32process.CREATE_NO_WINDOW
# It's pretty icky to have to do this, but since we're in a virtualenv, it's not too bad...
kwargs = { "executable_path": webdriver_path }
if "chromedriver" in webdriver_path:
options = webdriver.ChromeOptions()
options.set_headless( headless=True )
# OMG! The chromedriver looks for Chrome/Chromium in a hard-coded, fixed location (the default
# installation directory). We offer a way here to override this.
chrome_path = app.config.get( "CHROME_PATH" )
if chrome_path:
options.binary_location = chrome_path
kwargs["chrome_options"] = options
self.webdriver = webdriver.Chrome( **kwargs )
elif "geckodriver" in webdriver_path:
options = webdriver.FirefoxOptions()
options.set_headless( headless=True )
kwargs["firefox_options"] = options
kwargs["log_path"] = app.config.get( "GECKODRIVER_LOG",
os.path.join( tempfile.gettempdir(), "geckodriver.log" )
)
self.webdriver = webdriver.Firefox( **kwargs )
else:
raise SimpleError( "Can't identify webdriver: {}".format( webdriver_path ) )
return self
def __exit__( self, exc_type, exc_val, exc_tb ):
"""Clean up."""
if self.webdriver:
self.webdriver.quit()
def get_screenshot( self, html, window_size ):
"""Get a preview screenshot of the specified HTML."""
self.webdriver.set_window_size( window_size[0], window_size[1] )
with TempFile( extn=".html", mode="w" ) as html_tempfile:
# take a screenshot of the HTML
# NOTE: We could do some funky Javascript stuff to load the browser directly from the string,
# but using a temp file is straight-forward and pretty much guaranteed to work :-/
html_tempfile.write( html )
html_tempfile.close()
self.webdriver.get( "file://{}".format( html_tempfile.name ) )
with TempFile( extn=".png" ) as screenshot_tempfile:
screenshot_tempfile.close()
self.webdriver.save_screenshot( screenshot_tempfile.name )
img = Image.open( screenshot_tempfile.name )
# trim the screenshot (nb: we assume a white background)
bgd = Image.new( img.mode, img.size, (255,255,255,255) )
diff = ImageChops.difference( img, bgd )
bbox = diff.getbbox()
return img.crop( bbox )
# ---------------------------------------------------------------------
def change_extn( fname, extn ):
"""Change a filename's extension."""
return pathlib.Path( fname ).with_suffix( extn )

@ -18,7 +18,8 @@ from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import BASE_DIR, IS_FROZEN
from vasl_templates.webapp.files import vasl_mod
from vasl_templates.webapp.file_server.vasl_mod import SUPPORTED_VASL_MOD_VERSIONS
from vasl_templates.webapp.utils import TempFile, HtmlScreenshots, SimpleError
from vasl_templates.webapp.utils import TempFile, SimpleError
from vasl_templates.webapp.webdriver import WebDriver
_logger = logging.getLogger( "update_vsav" )
@ -132,60 +133,39 @@ def _save_snippets( snippets, fp ):
NOTE: We save the snippets as XML because Java :-/
"""
def get_html_size( snippet_id, html, window_size ):
"""Get the size of the specified HTML."""
start_time = time.time()
img = html_screenshots.get_screenshot( html, window_size )
elapsed_time = time.time() - start_time
width, height = img.size
_logger.debug( "Generated screenshot for %s (%.3fs): %dx%d", snippet_id, elapsed_time, width, height )
return width, height
def do_save_snippets( html_screenshots ): #pylint: disable=too-many-locals
def do_save_snippets( webdriver ): #pylint: disable=too-many-locals
"""Save the snippets."""
root = ET.Element( "snippets" )
for key,val in snippets.items():
for snippet_id,snippet_info in snippets.items():
# add the next snippet
auto_create = "true" if val["auto_create"] else "false"
elem = ET.SubElement( root, "snippet", id=key, autoCreate=auto_create )
elem.text = val["content"]
label_area = val.get( "label_area" )
auto_create = "true" if snippet_info["auto_create"] else "false"
elem = ET.SubElement( root, "snippet", id=snippet_id, autoCreate=auto_create )
elem.text = snippet_info["content"]
label_area = snippet_info.get( "label_area" )
if label_area:
elem.set( "labelArea", label_area )
# add the raw content
elem2 = ET.SubElement( elem, "rawContent" )
for node in val.get("raw_content",[]):
for node in snippet_info.get( "raw_content", [] ):
ET.SubElement( elem2, "phrase" ).text = node
# include the size of the snippet
if html_screenshots:
if webdriver:
try:
# NOTE: Screenshots take significantly longer for larger window sizes. Since most of our snippets
# will be small, we first try with a smaller window, and switch to a larger one if necessary.
max_width, max_height = 500, 500
max_width2, max_height2 = 1500, 1500
if key.startswith(
("ob_vehicles_ma_notes_","ob_vehicle_note_","ob_ordnance_ma_notes_","ob_ordnance_note_")
):
# nb: these tend to be large, don't bother with a smaller window
max_width, max_height = max_width2, max_height2
width, height = get_html_size( key, val["content"], (max_width,max_height) )
if (width >= max_width*9/10 or height >= max_height*9/10) \
and (max_width,max_height) != (max_width2,max_height2):
# NOTE: While it's tempting to set the browser window really large here, if the label ends up
# filling/overflowing the available space (e.g. because its width/height has been set to 100%),
# then the auto-created label will push any subsequent labels far down the map, possibly to
# somewhere unreachable. So, we set it somewhat more conservatively, so that if this happens,
# the user still has a chance to recover from it. Note that this doesn't mean that they can't
# have really large labels, it just affects the positioning of auto-created labels.
width, height = get_html_size( key, val["content"], (max_width2,max_height2) )
start_time = time.time()
img = webdriver.get_snippet_screenshot( snippet_id, snippet_info["content"] )
width, height = img.size
elapsed_time = time.time() - start_time
_logger.debug( "Generated screenshot for %s (%.3fs): %dx%d",
snippet_id, elapsed_time, width, height
)
# FUDGE! There's something weird going on in VASSAL e.g. "<table width=300>" gives us something
# very different to "<table style='width:300px;'>" :-/ Changing the font size also causes problems.
# The following fudging seems to give us something that's somewhat reasonable... :-/
if re.search( r"width:\s*?\d+?px", val["content"] ):
if re.search( r"width:\s*?\d+?px", snippet_info["content"] ):
width = int( width * 140 / 100 )
elem.set( "width", str(width) )
elem.set( "height", str(height) )
@ -201,8 +181,8 @@ def _save_snippets( snippets, fp ):
if app.config.get( "DISABLE_UPDATE_VSAV_SCREENSHOTS" ):
return do_save_snippets( None )
else:
with HtmlScreenshots() as html_screenshots:
return do_save_snippets( html_screenshots )
with WebDriver() as webdriver:
return do_save_snippets( webdriver )
def _parse_label_report( fname ):
"""Read the label report generated by the VASSAL shim."""

@ -0,0 +1,126 @@
""" Wrapper for a Selenium webdriver. """
import os
import tempfile
from selenium import webdriver
from PIL import Image, ImageChops
from vasl_templates.webapp import app
from vasl_templates.webapp.utils import TempFile, SimpleError
# ---------------------------------------------------------------------
class WebDriver:
"""Wrapper for a Selenium webdriver."""
def __init__( self ):
self.driver = None
def start_webdriver( self ):
"""Start the webdriver."""
# initialize
assert not self.driver
# locate the webdriver executable
webdriver_path = app.config.get( "WEBDRIVER_PATH" )
if not webdriver_path:
raise SimpleError( "No webdriver has been configured." )
# NOTE: If we are being run on Windows without a console (e.g. the frozen PyQt desktop app),
# Selenium will launch the webdriver in a visible DOS box :-( There's no way to turn this off,
# but it can be disabled by modifying the Selenium source code. Find the subprocess.Popen() call
# in $/site-packages/selenium/webdriver/common/service.py and add the following parameter:
# creationflags = 0x8000000 # win32process.CREATE_NO_WINDOW
# It's pretty icky to have to do this, but since we're in a virtualenv, it's not too bad...
# create the webdriver
kwargs = { "executable_path": webdriver_path }
if "chromedriver" in webdriver_path:
options = webdriver.ChromeOptions()
options.set_headless( headless=True )
# OMG! The chromedriver looks for Chrome/Chromium in a hard-coded, fixed location (the default
# installation directory). We offer a way here to override this.
chrome_path = app.config.get( "CHROME_PATH" )
if chrome_path:
options.binary_location = chrome_path
kwargs["chrome_options"] = options
self.driver = webdriver.Chrome( **kwargs )
elif "geckodriver" in webdriver_path:
options = webdriver.FirefoxOptions()
options.set_headless( headless=True )
kwargs["firefox_options"] = options
kwargs["log_path"] = app.config.get( "GECKODRIVER_LOG",
os.path.join( tempfile.gettempdir(), "geckodriver.log" )
)
self.driver = webdriver.Firefox( **kwargs )
else:
raise SimpleError( "Can't identify webdriver: {}".format( webdriver_path ) )
return self
def stop_webdriver( self ):
"""Stop the webdriver."""
assert self.driver
self.driver.quit()
self.driver = None
def get_screenshot( self, html, window_size, large_window_size=None ):
"""Get a preview screenshot of the specified HTML."""
def do_get_screenshot( fname ): #pylint: disable=missing-docstring
self.driver.save_screenshot( fname )
img = Image.open( fname )
# trim the screenshot (nb: we assume a white background)
bgd = Image.new( img.mode, img.size, (255,255,255,255) )
diff = ImageChops.difference( img, bgd )
bbox = diff.getbbox()
return img.crop( bbox )
with TempFile(extn=".html",mode="w") as html_tempfile, TempFile(extn=".png") as screenshot_tempfile:
# NOTE: We could do some funky Javascript stuff to load the browser directly from the string,
# but using a temp file is straight-forward and pretty much guaranteed to work :-/
html_tempfile.write( html )
html_tempfile.close()
self.driver.get( "file://{}".format( html_tempfile.name ) )
# take a screenshot of the HTML
screenshot_tempfile.close()
self.driver.set_window_size( window_size[0], window_size[1] )
img = do_get_screenshot( screenshot_tempfile.name )
if img.width > window_size[0]*9/10 or img.height > window_size[1]*9/10:
# the image may have been cropped - try again with a larger window size
if large_window_size:
self.driver.set_window_size( large_window_size[0], large_window_size[1] )
img = do_get_screenshot( screenshot_tempfile.name )
return img
def get_snippet_screenshot( self, snippet_id, snippet ):
"""Get a screenshot for an HTML snippet."""
# NOTE: Screenshots take significantly longer for larger window sizes. Since most of our snippets
# will be small, we first try with a smaller window, and switch to a larger one if necessary.
# NOTE: While it's tempting to set the larger window really large here, if the label ends up
# filling/overflowing the available space (e.g. because its width/height has been set to 100%),
# then the auto-created label will push any subsequent labels far down the map, possibly to
# somewhere unreachable. So, we set it somewhat more conservatively, so that if this happens,
# the user still has a chance to recover from it. Note that this doesn't mean that they can't
# have really large labels, it just affects the positioning of auto-created labels.
window_size, window_size2 = (500,500), (1500,1500)
if snippet_id.startswith(
("ob_vehicles_ma_notes_","ob_vehicle_note_","ob_ordnance_ma_notes_","ob_ordnance_note_")
):
# nb: these tend to be large, don't bother with a smaller window
window_size, window_size2 = window_size2, None
return self.get_screenshot( snippet, window_size, window_size2 )
def __enter__( self ):
self.start_webdriver()
return self
def __exit__( self, *args ):
self.stop_webdriver()
Loading…
Cancel
Save