diff --git a/README.md b/README.md index 7cb9fad..edd57ee 100644 --- a/README.md +++ b/README.md @@ -17,43 +17,43 @@ $ pip install anishot ## Usage ``` -$ anishot -Usage: -anishot.__main__: - --h: Window height - (default: '0') - (an integer) - --inp: Input screenshot image - --maxspeed: Max speed on scroll px/frame - (default: '200') - (an integer) - --out: Output antimated GIF - --pad: Padding on sides - (default: '0') - (an integer) - --rgb_bg: Background color - (default: '#ffffff') - --rgb_outline: Screenshot outline color - (default: '#e1e4e8') - --rgb_shadow: Screenshot shadow color - (default: '#999999') - --rgb_window: Window outline color - (default: '#e1e4e8') - --shadow_size: Shadow size - (default: '0') - (an integer) - --start_scale: Start scale - (default: '0.5') - (a number) - --stops: List of stops for scrolling - (default: '') - (a comma separated list) - --zoom_steps: Number of steps on initial zoom in - (default: '7') - (an integer) - --zoom_to: Point to zoom to - (default: '0') - (an integer) +$ anishot --help +usage: anishot [-h] [-p PAD] [-m MAXSPEED] [-s [STOPS [STOPS ...]]] + [--zoom-steps ZOOM_STEPS] [--start-scale START_SCALE] + [--zoom-to ZOOM_TO] [--shadow-size SHADOW_SIZE] + [--rgb-outline RGB_OUTLINE] [--rgb-background RGB_BACKGROUND] + [--rgb-shadow RGB_SHADOW] [--rgb-window RGB_WINDOW] + input output height + +Animates a long screenshot into a GIF + +positional arguments: + input Input screenshot image + output Output animated GIF + height Window height + +optional arguments: + -h, --help show this help message and exit + -p PAD, --pad PAD Padding on sides + -m MAXSPEED, --maxspeed MAXSPEED + Max speed on scroll px/frame + -s [STOPS [STOPS ...]], --stops [STOPS [STOPS ...]] + Max speed on scroll px/frame + --zoom-steps ZOOM_STEPS + Number of steps on initial zoom in + --start-scale START_SCALE + Start scale + --zoom-to ZOOM_TO Point to zoom to + --shadow-size SHADOW_SIZE + Shadow size + --rgb-outline RGB_OUTLINE + Screenshot outline color + --rgb-background RGB_BACKGROUND + Background color + --rgb-shadow RGB_SHADOW + Screenshot shadow color + --rgb-window RGB_WINDOW + Window outline color ``` The anishot at the top of this README was generated by: diff --git a/anishot/__main__.py b/anishot/__main__.py index 68729ca..19640d5 100644 --- a/anishot/__main__.py +++ b/anishot/__main__.py @@ -3,41 +3,57 @@ __copyright__ = 'Copyright 2018 Sourcerer' __author__ = 'Sergey Surkov' -import os +import argparse import sys -from absl import flags as gflags import imageio import numpy from PIL import Image from PIL.ImageDraw import Draw -gflags.DEFINE_string('inp', None, 'Input screenshot image') -gflags.DEFINE_string('out', None, 'Output antimated GIF') -gflags.DEFINE_integer('h', 0, 'Window height') -gflags.DEFINE_integer('pad', 0, 'Padding on sides') -gflags.DEFINE_integer('maxspeed', 200, 'Max speed on scroll px/frame') -gflags.DEFINE_list('stops', [], 'List of stops for scrolling') -gflags.DEFINE_integer('zoom_steps', 7, 'Number of steps on initial zoom in') -gflags.DEFINE_float('start_scale', .5, 'Start scale') -gflags.DEFINE_integer('zoom_to', 0, 'Point to zoom to') - -gflags.DEFINE_integer('shadow_size', 0, 'Shadow size') -gflags.DEFINE_string('rgb_outline', '#e1e4e8', 'Screenshot outline color') -gflags.DEFINE_string('rgb_bg', '#ffffff', 'Background color') -gflags.DEFINE_string('rgb_shadow', '#999999', 'Screenshot shadow color') -gflags.DEFINE_string('rgb_window', '#e1e4e8', 'Window outline color') - -gflags.register_validator('inp', os.path.exists, 'Input screenshot required') -gflags.register_validator('h', lambda v: v > 0, 'Window height required') - -F = gflags.FLAGS +ARGS = None + + +def argparser(): + parser = argparse.ArgumentParser( + description='Animates a long screenshot into a GIF') + parser.add_argument('input', type=argparse.FileType(), + help='Input screenshot image') + parser.add_argument('output', type=str, + help='Output animated GIF') + parser.add_argument('height', type=int, + help='Window height') + parser.add_argument('-p', '--pad', default=0, type=int, + help='Padding on sides') + parser.add_argument('-m', '--maxspeed', default=200, type=int, + help='Max speed on scroll px/frame') + parser.add_argument('-s', '--stops', nargs='*', default=[], + help='Max speed on scroll px/frame') + parser.add_argument('--zoom-steps', default=7, type=int, + help='Number of steps on initial zoom in') + parser.add_argument('--start-scale', default=5, type=int, + help='Start scale') + parser.add_argument('--zoom-to', default=0, type=int, + help='Point to zoom to') + parser.add_argument('--shadow-size', default=0, type=int, + help='Shadow size') + parser.add_argument('--rgb-outline', default='#e1e4e8', type=str, + help='Screenshot outline color') + parser.add_argument('--rgb-background', default='#ffffff', type=str, + help='Background color') + parser.add_argument('--rgb-shadow', default='#999999', type=str, + help='Screenshot shadow color') + parser.add_argument('--rgb-window', default='#e1e4e8', type=str, + help='Window outline color') + global ARGS + ARGS = parser.parse_args() def make_blank_frame(width): - return Image.new('RGB', (width + F.pad * 2, F.h), F.rgb_bg) + return Image.new('RGB', + (width + ARGS.pad * 2, ARGS.height), ARGS.rgb_background) def scale_image(image, scale): @@ -53,13 +69,14 @@ def render_frame(image, y, frame): draw = Draw(frame) # Draw shadow and image. - off = F.shadow_size - draw.rectangle([x + off, y + off, x + off + w, y + off + h], F.rgb_shadow) + off = ARGS.shadow_size + draw.rectangle( + [x + off, y + off, x + off + w, y + off + h], ARGS.rgb_shadow) frame.paste(image, (x, y)) - draw.rectangle([x, y, x + w, y + h], outline=F.rgb_outline) + draw.rectangle([x, y, x + w, y + h], outline=ARGS.rgb_outline) # Draw a frame border. - draw.rectangle([0, 0, fw - 1, fh - 1], outline=F.rgb_window) + draw.rectangle([0, 0, fw - 1, fh - 1], outline=ARGS.rgb_window) def add_frame(frame, duration, frames): @@ -68,19 +85,20 @@ def add_frame(frame, duration, frames): def make_zoomin(image, frames): w, h = image.size - scale = F.start_scale - step = (1 - scale) / (F.zoom_steps + 1) - start_y = -F.h / 10 / scale - for i in range(F.zoom_steps): + scale = ARGS.start_scale + step = (1 - scale) / (ARGS.zoom_steps + 1) + start_y = -ARGS.height / 10 / scale + for i in range(ARGS.zoom_steps): scaled = scale_image(image, scale) + if not scaled: + continue frame = make_blank_frame(w) - progress = (F.zoom_steps - i - 1) / (F.zoom_steps - 1) - y = -int( - (progress * start_y + (1 - progress) * F.zoom_to) * scale + .5) + progress = (ARGS.zoom_steps - i - 1) / (ARGS.zoom_steps - 1) + y = -int((progress * start_y + (1 - progress) * ARGS.zoom_to) * + scale + .5) render_frame(scaled, y, frame) add_frame(frame, .1 if i > 0 else 1.5, frames) - scale += step @@ -93,7 +111,8 @@ def add_scroll_frame(image, y, duration, frames): def make_scroll(image, frames): w, h = image.size - stops = [F.zoom_to] + list(map(int, F.stops)) + [h - F.h + F.pad] + stops = [ARGS.zoom_to] + \ + list(map(int, ARGS.stops)) + [h - ARGS.height + ARGS.pad] add_scroll_frame(image, stops[0], 2, frames) for i in range(len(stops) - 1): s0, s1 = stops[i:i + 2] @@ -103,34 +122,43 @@ def make_scroll(image, frames): while y < s1: add_scroll_frame(image, y, .01, frames) y += speed - speed = min(speed * 2, F.maxspeed) + speed = min(speed * 2, ARGS.maxspeed) add_scroll_frame(image, s1, 2, frames) +def process(): + image = Image.fromarray(imageio.imread(ARGS.input.name)) + frames = [] + + if ARGS.zoom_steps: + make_zoomin(image, frames) + make_scroll(image, frames) + + imageio.mimwrite(ARGS.output, + map(lambda f: numpy.array(f[0]), frames), + duration=list(map(lambda f: f[1], frames))) + + +def check(): + if ARGS.output[-4:] != '.gif': + raise ValueError("output must be a gif file") + + +def finish(): + msg = "Done! Generated file will be available at {}".format(ARGS.output) + print(msg) + + def main(): - argv = sys.argv try: - F(argv) - - image = Image.fromarray(imageio.imread(F.inp)) - frames = [] - - if F.zoom_steps: - make_zoomin(image, frames) - make_scroll(image, frames) - - imageio.mimwrite(F.out, - map(lambda f: numpy.array(f[0]), frames), - duration=list(map(lambda f: f[1], frames))) - except TypeError as e: - print('e: ', e) - print('Usage: %s' % F) - return 1 - except gflags.Error as e: - print('e: ', e) - print('Usage: %s' % F) - return 1 + argparser() + check() + process() + finish() + except ValueError: + print("Invalid output filename") + return 127 if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index d037f4f..8a76358 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -absl-py=0.2.2 imageio==2.3.0 -Pillow==5.2.0 +Pillow==5.2.0 \ No newline at end of file