From 129c1ecd584676dbff0f1d1e0943ee3fe59d7a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Thu, 17 Jun 2021 15:33:39 +0200 Subject: [PATCH] shot them all: compare shots --- bin/compare_them_all.py | 120 ++++++++++++++++++++++++++++++++++++++++ bin/index_template.html | 26 +++++++++ bin/shot_them_all.py | 31 +++++++---- 3 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 bin/compare_them_all.py create mode 100644 bin/index_template.html diff --git a/bin/compare_them_all.py b/bin/compare_them_all.py new file mode 100644 index 0000000..ad0be63 --- /dev/null +++ b/bin/compare_them_all.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +# Compare desktop/mobile shots of all themes from publik-base-theme. +# +# Usage: +# --input1-directory -- directory to find version 1 screenshots +# --input2-directory -- directory to find version 2 screenshots +# --output-directory DIRECTORY -- directory to store comparison results, defaults to results/ +# --overlay OVERLAY -- limit to given overlay ("none" to exclude overlays) +# --theme THEME -- theme to shot (substring match) +# +# Example: +# hobo-manage tenant_command runscript compare_them_all.py -d hobo.fred.local.0d.be +# + +import argparse +import os +import sys +from hobo.theme.utils import get_themes +from django.template import Template, Context +from PIL import Image, ImageDraw + +parser = argparse.ArgumentParser() +parser.add_argument('--input1-directory', dest='input1_directory', type=str, required=True) +parser.add_argument('--input2-directory', dest='input2_directory', type=str, required=True) +parser.add_argument('--output-directory', dest='output_directory', type=str, default='results') +parser.add_argument('--overlay', dest='overlay', type=str, default='none') +parser.add_argument('--theme', dest='theme', type=str) +args = parser.parse_args() + +if not os.path.exists(args.output_directory): + os.mkdir(args.output_directory) + + +result = [] + +for theme in get_themes(): + theme_id = theme['id'] + if args.overlay == 'all': + pass + elif args.overlay == 'none' and not theme.get('overlay'): + pass + elif args.overlay == theme.get('overlay'): + pass + else: + continue + if args.theme and args.theme not in theme['id']: + continue + sys.stderr.write("%-25s" % theme_id) + shots = [ + 'desktop', + 'mobile', + 'mobile-horizontal', + ] + + def process_region(image, x, y, width, height): + region_total = 0 + # This can be used as the sensitivity factor, the larger it is the less sensitive the comparison + factor = 100 + for coordinate_y in range(y, y + height): + for coordinate_x in range(x, x + width): + try: + pixel = image.getpixel((coordinate_x, coordinate_y)) + region_total += sum(pixel) / 4 + except Exception: + return + return region_total / factor + + theme_data = { + 'label': theme['label'], + 'shots': [], + } + + for shot in shots: + filepath_1 = os.path.join(args.input1_directory, '%s-%s.png' % (theme_id, shot)) + filepath_2 = os.path.join(args.input2_directory, '%s-%s.png' % (theme_id, shot)) + filepath_output = os.path.join(args.output_directory, '%s-%s.png' % (theme_id, shot)) + try: + screenshot_1 = Image.open(filepath_1) + screenshot_2 = Image.open(filepath_2) + except FileNotFoundError: + continue + width, height = screenshot_1.size + + columns = 60 + rows = 80 + screen_width, screen_height = screenshot_1.size + + block_width = ((screen_width - 1) // columns) + 1 + block_height = ((screen_height - 1) // rows) + 1 + + for y in range(0, screen_height, block_height + 1): + for x in range(0, screen_width, block_width + 1): + region_1 = process_region(screenshot_1, x, y, block_width, block_height) + region_2 = process_region(screenshot_2, x, y, block_width, block_height) + + if region_1 is not None and region_2 is not None and region_1 != region_2: + draw = ImageDraw.Draw(screenshot_1) + draw.rectangle((x, y, x + block_width, y + block_height), outline="red") + + screenshot_1.save(filepath_output) + theme_data['shots'].append({ + 'label': shot, + 'width': width, + 'height': height, + 'before': '../%s' % filepath_1, + 'after': '../%s' % filepath_2, + 'diff': '../%s' % filepath_output, + }) + result.append(theme_data) + +sys.stderr.write(u'\n') + +with open('index_template.html', 'r') as f: + template = Template(f.read()) + context = Context({'themes': result}) + html = template.render(context) + +with open(os.path.join(args.output_directory, 'index.html'), 'w') as f: + f.write(html) diff --git a/bin/index_template.html b/bin/index_template.html new file mode 100644 index 0000000..7db64f6 --- /dev/null +++ b/bin/index_template.html @@ -0,0 +1,26 @@ + + + + + + + + {% for theme in themes %} +

{{ theme.label }}

+ {% for shot in theme.shots %} +

{{ shot.label }}

+
+ + +
+
Diff
+ + {% endfor %} + {% endfor %} + + + diff --git a/bin/shot_them_all.py b/bin/shot_them_all.py index 8940799..84d3332 100644 --- a/bin/shot_them_all.py +++ b/bin/shot_them_all.py @@ -3,14 +3,15 @@ # Take desktop/mobile shots of all themes from publik-base-theme. # # Usage: -# --directory DIRECTORY -- directory to store shots, defaults to shots/ -# --no-headless -- run in an actual browser window -# --no-memcached -- do not restart memcached after theme changes -# --overlay OVERLAY -- limit to given overlay ("none" to exclude overlays) -# --reshot -- retake existing shots -# --timeout TIMEOUT -- timeout between shots (time for hobo deploy) -# --theme THEME -- theme to shot (substring match) -# URL -- specific URL to shot +# --directory DIRECTORY -- directory to store shots, defaults to shots/ +# --no-headless -- run in an actual browser window +# --no-memcached -- do not restart memcached after theme changes +# --overlay OVERLAY -- limit to given overlay ("none" to exclude overlays) +# --reshot -- retake existing shots +# --timeout TIMEOUT -- timeout between shots (time for hobo deploy) +# --cell-timeout TIMEOUT -- timeout after url loading (time for ajax cells loading) +# --theme THEME -- theme to shot (substring match) +# URL -- specific URL to shot # # Example: # hobo-manage tenant_command runscript shot_them_all.py -d hobo.fred.local.0d.be @@ -31,9 +32,10 @@ parser.add_argument('--no-memcached', dest='no_memcached', action='store_true') parser.add_argument('--overlay', dest='overlay', type=str, default='none') parser.add_argument('--reshot', dest='reshot', action='store_true') parser.add_argument('--timeout', dest='timeout', type=int, default=20) # seconds +parser.add_argument('--cell-timeout', dest='cell_timeout', type=int, default=0) # seconds parser.add_argument('--theme', dest='theme', type=str) parser.add_argument('url', metavar='URL', type=str, nargs='?', - default='https://auquo.fred.local.0d.be/contactez-nous/inscription-sur-les-listes/') + default='https://auquo.fred.local.0d.be/contactez-nous/inscription-sur-les-listes/') args = parser.parse_args() if not os.path.exists(args.directory): @@ -54,7 +56,7 @@ for theme in get_themes(): pass else: continue - if args.theme and not args.theme in theme['id']: + if args.theme and args.theme not in theme['id']: continue sys.stderr.write("%-25s" % theme_id) shots = [ @@ -75,9 +77,18 @@ for theme in get_themes(): if not args.no_memcached: os.system('sudo service memcached restart') browser.get(args.url) + for i in range(args.cell_timeout): + sys.stderr.write('.') + time.sleep(1) + + def size(value): + return browser.execute_script('return document.body.parentNode.scroll' + value) + for shot in shots: browser.set_window_size(shot[0], shot[1]) + browser.set_window_size(shot[0], size('Height')) browser.save_screenshot(os.path.join(args.directory, '%s-%s.png' % (theme_id, shot[2]))) + sys.stderr.write(u' 📸 \n') browser.close()