from doit import create_after from glob import glob from itertools import chain def reltouch(source_filename, dest_filename): from os import stat, utime stat_res = stat(source_filename) utime(dest_filename, ns=(stat_res.st_atime_ns, stat_res.st_mtime_ns)) def resize_image(basename, width, image_format): from PIL import Image with Image.open('assets/{}.png'.format(basename)) as im: if 'A' in im.getbands() and image_format != 'jpeg': im = im.convert('RGBA') else: im = im.convert('RGB') height = im.height * width // im.width new = im.resize((width, height), resample=Image.LANCZOS) if image_format == 'jpeg': kwargs = dict( optimize=True, progressive=True, quality=80, ) elif image_format == 'webp': kwargs = dict( quality=79, ) elif image_format == 'png': kwargs = dict( optimize=True, ) new.save('static/{}-{}.{}'.format(basename, width, image_format), **kwargs) reltouch('assets/{}.png'.format(basename), 'static/{}-{}.{}'.format(basename, width, image_format)) def task_logotype(): """resize and convert logotype""" widths = (200, 400, 600, 800) image_formats = ('jpeg', 'webp') for width in widths: for image_format in image_formats: yield dict( name='{}.{}'.format(width, image_format), actions=[(resize_image, ('logotype', width, image_format))], targets=[f'static/logotype-{width}.{image_format}'], file_dep=['assets/logotype.png'], clean=True, ) def task_service_icon(): """resize and convert service icons""" widths = (20, 40, 80) formats = ('webp', 'png') for width in widths: for image_format in formats: for basename in ('twitter', 'mastodon'): yield dict( name='{}-{}.{}'.format(basename, width, image_format), actions=[(resize_image, (basename, width, image_format))], targets=[ 'static/{}-{}.{}'.format(basename, width, image_format)], file_dep=['assets/{}.png'.format(basename)], clean=True, ) def task_copy(): "copy assets verbatim" assets = ('icon.png', 'logotype.png') def do_the_thing(src, dst): from shutil import copy copy(src, dst) reltouch(src, dst) for asset in assets: src = 'assets/{}'.format(asset) dst = 'static/{}'.format(asset) yield dict( name=asset, actions=[(do_the_thing, (src, dst))], targets=[dst], file_dep=[src], clean=True, ) def task_minify_css(): """minify css file with csscompressor""" from csscompressor import compress def minify(): with open('assets/styles.css') as in_: with open('static/styles.css', 'w') as out: out.write(compress(in_.read())) reltouch('assets/styles.css', 'static/styles.css') return dict( actions=[minify], targets=['static/styles.css'], file_dep=['assets/styles.css'], clean=True, ) def task_rollup(): """rollup javascript bundle""" filenames = ['settings.js'] for filename in filenames: src = 'assets/{}'.format(filename) dst = 'static/{}'.format(filename) yield dict( name=filename, file_dep=list(chain( # fuck it glob('assets/*.js'), glob('components/*.html'))), targets=[dst], clean=True, actions=[ ['node_modules/.bin/rollup', '-c', '-i', src, '-o', dst, '-f', 'iife'], ], ) @create_after('logotype') @create_after('service_icon') @create_after('copy') @create_after('minify_css') @create_after('rollup') def task_compress(): """ make gzip and brotli compressed versions of each static file for the server to lazily serve """ files = chain( glob('static/*.css'), glob('static/*.js'), glob('static/*.jpeg'), glob('static/*.png'), glob('static/*.webp'), ) def compress_brotli(filename): import brotli with open(filename, 'rb') as in_: with open(filename + '.br', 'wb') as out: out.write(brotli.compress(in_.read())) reltouch(filename, filename+'.br') def compress_gzip(filename): import gzip with open(filename, 'rb') as in_: with gzip.open(filename + '.gz', 'wb') as out: out.write(in_.read()) reltouch(filename, filename+'.gz') for filename in files: yield dict( file_dep=(filename,), targets=(filename+'.br',), name=filename+'.br', actions=[(compress_brotli, (filename,))], clean=True, ) yield dict( file_dep=(filename,), targets=(filename+'.gz',), name=filename+'.gz', actions=[(compress_gzip, (filename,))], clean=True, ) if __name__ == '__main__': import doit doit.run(globals())