diff --git a/.gitignore b/.gitignore index 1e91379..3f9119e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,22 @@ +# Ignore package setup generated files *.egg-info/ -*.mypy_cache -*/.mypy_cache -*.__pycache__ +build/ +dist/ +# Ignore pycache */__pycache__ +# Ignore script generated temporary directory */Glitched GIF +# Ignore venv +glitchenv/ +# Ignore vscode specific files .vscode/ +# Ignore script generated outputs Collections/ -build/ -dist/ -glitch_env/ +# Ignore some file formats (usually present for the user) *.png *.gif *.jpg *.info +# Except the test.png and test.gif files !test.png +!test.gif diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c45ec..d7cff58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,3 +126,12 @@ The version is only checked once every few days and saved into a local file. This file is checked afterwards. Eliminates the need to send request to pypi * Add `--version` argument to the commandline script + +## Version 1.0.0 - **MAJOR** +* NEW `glitch_image` and `glitch_gif` in `glitch_this.py`:- + * `seed`: Set a custom seed to be used by `random`, for generating similar images across runs +* NEW parameters for `commandline.py`:- + * `-sd, --seed`: Set a custom seed to be used by `random`, for generating similar images across runs +* Cleanup the codebase using fstrings +* Add FULL **typing support** for providing a better experience to library users +* Fix undefined variable in `glitch_gif` diff --git a/README.md b/README.md index 49e2dd1..2f8fc45 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Checkout a web demo right [here](https://github.com/pahefu/web-glitch-this), cou * Customize the **number of frames** in a GIF as well as their **duration** - all from the comfort of your terminal! * Set how many times the GIF should **loop**! +* Set your own custom **seed** for a predictable RNG! ## Changelog View the changelog [here](https://github.com/TotallyNotChase/glitch-this/blob/master/CHANGELOG.md) diff --git a/glitch_this/commandline.py b/glitch_this/commandline.py index c653898..285c122 100644 --- a/glitch_this/commandline.py +++ b/glitch_this/commandline.py @@ -1,26 +1,33 @@ #!/usr/bin/env python3 -import os, argparse +import argparse +import os +from datetime import datetime from pathlib import Path from time import time +from typing import Dict + from glitch_this import ImageGlitcher -def read_version(): - with open(version_filepath, 'r') as file: - content = file.read() + +def read_version() -> str: + with open(version_filepath, 'r') as version_file: + content = version_file.read() return content.strip() -def write_version(version): - with open(version_filepath, 'w') as file: - file.write(version + '\n') -def is_expired(filepath): +def write_version(version: str): + with open(version_filepath, 'w') as version_file: + version_file.write(version + '\n') + + +def is_expired(filepath: str) -> bool: # Check if the file has been created 2 weeks prior - from datetime import datetime file_creation = datetime.fromtimestamp(os.stat(filepath).st_mtime) now = datetime.now() return (now - file_creation).days > 14 -def is_latest(version): + +def is_latest(version: str) -> bool: # Check pypi for the latest version number from urllib import request import json @@ -30,7 +37,8 @@ def is_latest(version): else: # Either version log does not exist or is outdated try: - contents = request.urlopen('https://pypi.org/pypi/glitch-this/json').read() + contents = request.urlopen( + 'https://pypi.org/pypi/glitch-this/json').read() except: # Connection issue # Silenty return True, update check failed @@ -39,22 +47,22 @@ def is_latest(version): latest_version = data['info']['version'] write_version(latest_version) - print('Current version: {} | Latest version: {}'.format(version, latest_version)) + print(f'Current version: {version} | Latest version: {latest_version}') return version == latest_version -def get_help(glitch_min, glitch_max): + +def get_help(glitch_min: float, glitch_max: float) -> Dict: help_text = dict() help_text['path'] = 'Relative or Absolute string path to source image' - help_text['level'] = 'Number between {} and {}, inclusive, representing amount of glitchiness'.format(glitch_min, - glitch_max) + help_text['level'] = f'Number between {glitch_min} and {glitch_max}, inclusive, representing amount of glitchiness' help_text['color'] = 'Include if you want to add color offset' help_text['scan'] = 'Include if you want to add scan lines effect\nDefaults to False' + help_text['seed'] = 'Set a random seed for generating similar images across runs' help_text['gif'] = 'Include if you want output to be a GIF' help_text['frames'] = 'Number of frames to include in output GIF, default - 23' help_text['step'] = 'Glitch every step\'th frame of output GIF, default - 1 (every frame)' help_text['increment'] = 'Increment glitch_amount by given value after glitching every frame of output GIF' - help_text['cycle'] = 'Include if glitch_amount should be cycled back to {} or {} if it over/underflows'.format(glitch_min, - glitch_max) + help_text['cycle'] = f'Include if glitch_amount should be cycled back to {glitch_min} or {glitch_max} if it over/underflows' help_text['duration'] = 'How long to display each frame (in centiseconds), default - 200' help_text['relative_duration'] = 'Multiply given value to input GIF\'s original duration and use that as duration' help_text['loop'] = 'How many times the glitched GIF should loop, default - 0 (infinite loop)' @@ -63,18 +71,19 @@ def get_help(glitch_min, glitch_max): help_text['out'] = 'Explcitly supply full/relative path to output file' return help_text + def main(): glitch_min, glitch_max = 0.1, 10.0 - version = ImageGlitcher.__version__ + current_version = ImageGlitcher.__version__ help_text = get_help(glitch_min, glitch_max) # Add commandline arguments parser - argparser = argparse.ArgumentParser(description= - 'glitch_this: Glitchify images and GIFs, with highly customizable options!\n\n' + argparser = argparse.ArgumentParser(description='glitch_this: Glitchify images and GIFs, with highly customizable options!\n\n' '* Website: https://github.com/TotallyNotChase/glitch-this \n' - '* Version: ' + version + '\n' + f'* Version: {current_version}\n' '* Changelog: https://github.com/TotallyNotChase/glitch-this/blob/master/CHANGELOG.md', formatter_class=argparse.RawTextHelpFormatter) - argparser.add_argument('--version', action='version', version='glitch_this {}'.format(version)) + argparser.add_argument('--version', action='version', + version=f'glitch_this {current_version}') argparser.add_argument('src_img_path', metavar='Image_Path', type=str, help=help_text['path']) argparser.add_argument('glitch_level', metavar='Glitch_Level', type=float, @@ -85,6 +94,12 @@ def main(): help=help_text['scan']) argparser.add_argument('-g', '--gif', dest='gif', action='store_true', help=help_text['gif']) + argparser.add_argument('-ig', '--inputgif', dest='input_gif', action='store_true', + help=help_text['inputgif']) + argparser.add_argument('-f', '--force', dest='force', action='store_true', + help=help_text['force']) + argparser.add_argument('-sd', '--seed', dest='seed', metavar='Seed', type=float, default=None, + help=help_text['seed']) argparser.add_argument('-fr', '--frames', dest='frames', metavar='Frames', type=int, default=23, help=help_text['frames']) argparser.add_argument('-st', '--step', dest='step', metavar='Step', type=int, default=1, @@ -99,10 +114,6 @@ def main(): help=help_text['relative_duration']) argparser.add_argument('-l', '--loop', dest='loop', metavar='Loop_Count', type=int, default=0, help=help_text['loop']) - argparser.add_argument('-ig', '--inputgif', dest='input_gif', action='store_true', - help=help_text['inputgif']) - argparser.add_argument('-f', '--force', dest='force', action='store_true', - help=help_text['force']) argparser.add_argument('-o', '--outfile', dest='outfile', metavar='Outfile_path', type=str, help=help_text['out']) args = argparser.parse_args() @@ -128,11 +139,12 @@ def main(): # Overwrite the previous values out_path, out_file = os.path.split(Path(args.outfile)) if out_path != '' and not os.path.exists(out_path): - raise Exception('Given outfile path, ' + out_path + ', does not exist') + raise Exception('Given outfile path, ' + + out_path + ', does not exist') # The extension in user provided outfile path is ignored out_filename = out_file.rsplit('.', 1)[0] # Now create the full path - full_path = os.path.join(out_path, '{}.{}'.format(out_filename, out_fileex)) + full_path = os.path.join(out_path, f'{out_filename}.{out_fileex}') if os.path.exists(full_path) and not args.force: raise Exception(full_path + ' already exists\nCannot overwrite ' 'existing file unless -f or --force is included\nProgram Aborted') @@ -149,6 +161,7 @@ def main(): cycle=args.cycle, scan_lines=args.scan_lines, color_offset=args.color, + seed=args.seed, gif=args.gif, frames=args.frames, step=args.step) @@ -159,11 +172,13 @@ def main(): cycle=args.cycle, scan_lines=args.scan_lines, color_offset=args.color, + seed=args.seed, step=args.step) # Set args.gif to true if it isn't already in this case args.gif = True # Set args.duration to src_duration * relative duration, if one was given - args.duration = args.duration if not args.rel_duration else int(args.rel_duration * src_duration) + args.duration = args.duration if not args.rel_duration else int( + args.rel_duration * src_duration) t1 = time() # End of glitching t2 = time() @@ -174,21 +189,22 @@ def main(): print('Glitched Image saved in "{}"'.format(full_path)) else: glitch_img[0].save(full_path, - format='GIF', - append_images=glitch_img[1:], - save_all=True, - duration=args.duration, - loop=args.loop, - compress_level=3) + format='GIF', + append_images=glitch_img[1:], + save_all=True, + duration=args.duration, + loop=args.loop, + compress_level=3) t3 = time() - print('Glitched GIF saved in "{}"\nFrames = {}, Duration = {}, Loop = {}'.format(full_path, args.frames, args.duration, args.loop)) - print('Time taken to glitch: ' + str(t1 - t0)) - print('Time taken to save: ' + str(t3 - t2)) - print('Total Time taken: ' + str(t3 - t0)) + print( + f'Glitched GIF saved in "{full_path}"\nFrames = {args.frames}, Duration = {args.duration}, Loop = {args.loop}') + print(f'Time taken to glitch: {t1 - t0}') + print(f'Time taken to save: {t3 - t2}') + print(f'Total Time taken: {t3 - t0}') # Let the user know if new version is available - if not is_latest(version): + if not is_latest(current_version): print('A new version of "glitch-this" is available. Please consider upgrading via `pip3 install --upgrade glitch-this`') -if __name__=='__main__': +if __name__ == '__main__': main() diff --git a/glitch_this/glitch_this.py b/glitch_this/glitch_this.py index 4b54834..a8074c0 100644 --- a/glitch_this/glitch_this.py +++ b/glitch_this/glitch_this.py @@ -1,13 +1,17 @@ -import os, shutil -import numpy as np -from random import randint +import os +import shutil +import random from decimal import getcontext, Decimal +from typing import List, Optional, Tuple, Union + +import numpy as np from PIL import Image, ImageSequence + class ImageGlitcher: -# Handles Image/GIF Glitching Operations + # Handles Image/GIF Glitching Operations - __version__ = '0.1.5' + __version__ = '1.0.0' def __init__(self): # Setting up global variables needed for glitching @@ -27,9 +31,11 @@ def __init__(self): self.glitch_max = 10.0 self.glitch_min = 0.1 - def __isgif(self, img): + def __isgif(self, img: Union[str, Image.Image]) -> bool: # Returns true if input image is a GIF and/or animated if isinstance(img, str): + if not os.path.isfile(img): + return False img = Image.open(img) index = 0 for _ in ImageSequence.Iterator(img): @@ -39,7 +45,7 @@ def __isgif(self, img): return True return False - def __open_image(self, img_path): + def __open_image(self, img_path: str) -> Image.Image: # Returns an Image object # Will throw exception if img_path doesn't point to Image if img_path.endswith('.gif'): @@ -52,7 +58,7 @@ def __open_image(self, img_path): # Otherwise convert it to RGB return Image.open(img_path).convert('RGB') - def __fetch_image(self, src_img, gif_allowed): + def __fetch_image(self, src_img: Union[str, Image.Image], gif_allowed: bool) -> Image.Image: """ The following code resolves whether input was a path or an Image Then returns an Image object @@ -101,36 +107,60 @@ def __fetch_image(self, src_img, gif_allowed): raise Exception('Wrong format') return img - def glitch_image(self, src_img, glitch_amount, glitch_change=0.0, cycle=False, color_offset=False, scan_lines=False, gif=False, frames=23, step=1): + def glitch_image(self, src_img: Union[str, Image.Image], glitch_amount: Union[int, float], seed: Optional[Union[int, float]] = None, glitch_change: Union[int, float] = 0.0, + color_offset: bool = False, scan_lines: bool = False, gif: bool = False, cycle: bool = False, frames: int = 23, step: int = 1) -> Union[Image.Image, List[Image.Image]]: """ Sets up values needed for glitching the image + Returns created Image object if gif=False + Returns list of Image objects if gif=True PARAMETERS:- + src_img: Either the path to input Image or an Image object itself + glitch_amount: Level of glitch intensity, [0.1, 10.0] (inclusive) glitch_change: Increment/Decrement in glitch_amount after every glitch + cycle: Whether or not to cycle glitch_amount back to glitch_min or glitch_max if it over/underflows + color_offset: Specify True if color_offset effect should be applied + scan_lines: Specify True if scan_lines effect should be applied + gif: True if output should be ready to be saved as GIF + frames: How many glitched frames should be generated for GIF + step: Glitch every step'th frame, defaults to 1 (i.e all frames) + + seed: Set a random seed for generating similar images across runs, + defaults to None (random seed). """ + # Sanity checking the inputs if not ((isinstance(glitch_amount, float) - or isinstance(glitch_amount, int)) + or isinstance(glitch_amount, int)) and self.glitch_min <= glitch_amount <= self.glitch_max): raise ValueError('glitch_amount parameter must be a positive number ' - 'in range {} to {}, inclusive'.format(self.glitch_min, - self.glitch_max)) + f'in range {self.glitch_min} to {self.glitch_max}, inclusive') if not ((isinstance(glitch_change, float) or isinstance(glitch_change, int)) and -self.glitch_max <= glitch_change <= self.glitch_max): - raise ValueError('glitch_change parameter must be a number between 0.0 and 10.0 or -0.0 and -10.0') + raise ValueError( + f'glitch_change parameter must be a number between {-self.glitch_max} and {self.glitch_max}, inclusive') + if seed and not (isinstance(seed, float) or isinstance(seed, int)): + raise ValueError( + f'seed parameter must be a number') + if not (frames > 0 and isinstance(frames, int)): + raise ValueError( + 'frames param must be a positive integer value greater than 0') + if not step > 0 or not isinstance(step, int): + raise ValueError( + 'step parameter must be a positive integer value greater than 0') if not isinstance(cycle, bool): raise ValueError('cycle param must be a boolean') if not isinstance(color_offset, bool): @@ -139,21 +169,23 @@ def glitch_image(self, src_img, glitch_amount, glitch_change=0.0, cycle=False, c raise ValueError('scan_lines param must be a boolean') if not isinstance(gif, bool): raise ValueError('gif param must be a boolean') - if not (frames > 0 and isinstance(frames, int)): - raise ValueError('frames param must be a positive integer value greater than 0') - if not step > 0 or not isinstance(step, int): - raise ValueError('step parameter must be a positive integer value greater than 0') + + self.seed = seed + if self.seed: + # Set the seed if it was given + self.__reset_rng_seed() try: # Get Image, whether input was an str path or Image object # GIF input is NOT allowed in this method - img = self.__fetch_image(src_img, False) + img = self.__fetch_image(src_img, gif_allowed=False) except FileNotFoundError: # Throw DETAILED exception here (Traceback will be present from previous exceptions) - raise FileNotFoundError('No image found at given path: ' + src_img) + raise FileNotFoundError(f'No image found at given path: {src_img}') except: # Throw DETAILED exception here (Traceback will be present from previous exceptions) - raise Exception('File format not supported - must be a non-animated image file') + raise Exception( + 'File format not supported - must be a non-animated image file') # Fetching image attributes self.pixel_tuple_len = len(img.getbands()) @@ -192,12 +224,14 @@ def glitch_image(self, src_img, glitch_amount, glitch_change=0.0, cycle=False, c # Other frames will be appended as they are glitched_imgs.append(img.copy()) continue - glitched_img = self.__get_glitched_img(glitch_amount, color_offset, scan_lines) + glitched_img = self.__get_glitched_img( + glitch_amount, color_offset, scan_lines) file_path = os.path.join(self.gif_dirpath, 'glitched_frame.png') glitched_img.save(file_path, compress_level=3) glitched_imgs.append(Image.open(file_path).copy()) # Change glitch_amount by given value - glitch_amount = self.__change_glitch(glitch_amount, glitch_change, cycle) + glitch_amount = self.__change_glitch( + glitch_amount, glitch_change, cycle) # Set decimal precision back to original value getcontext().prec = original_prec @@ -205,7 +239,8 @@ def glitch_image(self, src_img, glitch_amount, glitch_change=0.0, cycle=False, c shutil.rmtree(self.gif_dirpath) return glitched_imgs - def glitch_gif(self, src_gif, glitch_amount, glitch_change=0.0, cycle=False, color_offset=False, scan_lines=False, step=1): + def glitch_gif(self, src_gif: Union[str, Image.Image], glitch_amount: Union[int, float], seed: Union[int, float] = None, glitch_change: Union[int, float] = 0.0, + color_offset: bool = False, scan_lines: bool = False, gif: bool = False, cycle: bool = False, step=1) -> Tuple[List[Image.Image], float, int]: """ Glitch each frame of input GIF Returns the following: @@ -217,7 +252,7 @@ def glitch_gif(self, src_gif, glitch_amount, glitch_change=0.0, cycle=False, col NOTE: This is a time consuming process, especially for large GIFs with many frames PARAMETERS:- - src_img: Either the path to input Image or an Image object itself + src_gif: Either the path to input Image or an Image object itself glitch_amount: Level of glitch intensity, [0.1, 10.0] (inclusive) glitch_change: Increment/Decrement in glitch_amount after every glitch @@ -226,18 +261,27 @@ def glitch_gif(self, src_gif, glitch_amount, glitch_change=0.0, cycle=False, col color_offset: Specify True if color_offset effect should be applied scan_lines: Specify True if scan_lines effect should be applied step: Glitch every step'th frame, defaults to 1 (i.e all frames) + seed: Set a random seed for generating similar images across runs, + defaults to None (random seed) """ + # Sanity checking the params if not ((isinstance(glitch_amount, float) - or isinstance(glitch_amount, int)) + or isinstance(glitch_amount, int)) and self.glitch_min <= glitch_amount <= self.glitch_max): - raise ValueError('glitch_amount parameter must be a positive number '\ - 'in range {} to {}, inclusive'.format(self.glitch_min, - self.glitch_max)) + raise ValueError('glitch_amount parameter must be a positive number ' + f'in range {self.glitch_min} to {self.glitch_max}, inclusive') if not ((isinstance(glitch_change, float) or isinstance(glitch_change, int)) and -self.glitch_max <= glitch_change <= self.glitch_max): - raise ValueError('glitch_change parameter must be a number between 0.0 and 10.0 or -0.0 and -10.0') + raise ValueError( + f'glitch_change parameter must be a number between {-self.glitch_max} and {self.glitch_max}, inclusive') + if seed and not (isinstance(seed, float) or isinstance(seed, int)): + raise ValueError( + f'seed parameter must be a number') + if not step > 0 or not isinstance(step, int): + raise ValueError( + 'step parameter must be a positive integer value greater than 0') if not isinstance(cycle, bool): raise ValueError('cycle param must be a boolean') if not isinstance(color_offset, bool): @@ -245,17 +289,21 @@ def glitch_gif(self, src_gif, glitch_amount, glitch_change=0.0, cycle=False, col if not isinstance(scan_lines, bool): raise ValueError('scan_lines param must be a boolean') if not self.__isgif(src_gif): - raise Exception('Input image must be a path to a GIF or be a GIF Image object') - if not step > 0 or not isinstance(step, int): - raise ValueError('step parameter must be a positive integer value greater than 0') + raise Exception( + 'Input image must be a path to a GIF or be a GIF Image object') + + self.seed = seed + if self.seed: + # Set the seed if it was given + self.__reset_rng_seed() try: # Get Image, whether input was an str path or Image object # GIF input is allowed in this method - gif = self.__fetch_image(src_gif, True) + gif = self.__fetch_image(src_gif, gif_allowed=True) except FileNotFoundError: # Throw DETAILED exception here (Traceback will be present from previous exceptions) - raise FileNotFoundError('No image found at given path: ' + src_img) + raise FileNotFoundError(f'No image found at given path: {src_gif}') except: # Throw DETAILED exception here (Traceback will be present from previous exceptions) raise Exception('File format not supported - must be an image file') @@ -288,12 +336,14 @@ def glitch_gif(self, src_gif, glitch_amount, glitch_change=0.0, cycle=False, col glitched_imgs.append(Image.open(src_frame_path).copy()) i += 1 continue - glitched_img = self.glitch_image(src_frame_path, glitch_amount, color_offset=color_offset, scan_lines=scan_lines) + glitched_img: Image.Image = self.glitch_image(src_frame_path, glitch_amount, + color_offset=color_offset, scan_lines=scan_lines) file_path = os.path.join(self.gif_dirpath, 'glitched_frame.png') glitched_img.save(file_path, compress_level=3) glitched_imgs.append(Image.open(file_path).copy()) # Change glitch_amount by given value - glitch_amount = self.__change_glitch(glitch_amount, glitch_change, cycle) + glitch_amount = self.__change_glitch( + glitch_amount, glitch_change, cycle) i += 1 # Set decimal precision back to original value @@ -302,30 +352,39 @@ def glitch_gif(self, src_gif, glitch_amount, glitch_change=0.0, cycle=False, col shutil.rmtree(self.gif_dirpath) return glitched_imgs, duration / i, i - def __change_glitch(self, glitch_amount, glitch_change, cycle): + def __change_glitch(self, glitch_amount: Union[int, float], glitch_change: Union[int, float], cycle: bool) -> float: # A function to change glitch_amount by given increment/decrement glitch_amount = float(Decimal(glitch_amount) + Decimal(glitch_change)) # glitch_amount must be between glith_min and glitch_max if glitch_amount < self.glitch_min: # If it's less, it will be cycled back to max when cycle=True # Otherwise, it'll stay at the least possible value -> glitch_min - glitch_amount = float(Decimal(self.glitch_max) + Decimal(glitch_amount)) if cycle else self.glitch_min + glitch_amount = float( + Decimal(self.glitch_max) + Decimal(glitch_amount)) if cycle else self.glitch_min if glitch_amount > self.glitch_max: # If it's more, it will be cycled back to min when cycle=True # Otherwise, it'll stay at the max possible value -> glitch_max - glitch_amount = float(Decimal(glitch_amount) % Decimal(self.glitch_max)) if cycle else self.glitch_max + glitch_amount = float(Decimal(glitch_amount) % Decimal( + self.glitch_max)) if cycle else self.glitch_max return glitch_amount - def __get_glitched_img(self, glitch_amount, color_offset, scan_lines): + def __get_glitched_img(self, glitch_amount: Union[int, float], color_offset: int, scan_lines: bool) -> Image.Image: """ Glitches the image located at given path Intensity of glitch depends on glitch_amount """ max_offset = int((glitch_amount ** 2 / 100) * self.img_width) doubled_glitch_amount = int(glitch_amount * 2) - for _ in range(0, doubled_glitch_amount): + for shift_number in range(0, doubled_glitch_amount): + + if self.seed: + # This is not deterministic as glitch amount changes the amount of shifting, + # so get the same values on each iteration on a new pseudo-seed that is + # offseted by the index we're iterating + self.__reset_rng_seed(offset=shift_number) + # Setting up offset needed for the randomized glitching - current_offset = randint(-max_offset, max_offset) + current_offset = random.randint(-max_offset, max_offset) if current_offset == 0: # Can't wrap left OR right when offset is 0, End of Array @@ -341,11 +400,20 @@ def __get_glitched_img(self, glitch_amount, color_offset, scan_lines): # Wrap around the lost pixel data from the left self.__glitch_right(current_offset) + if self.seed: + # Get the same channels on the next call, we have to reset the rng seed + # as the previous loop isn't fixed in size of iterations and depends on glitch amount + self.__reset_rng_seed() + if color_offset: + # Get the next random channel we'll offset, needs to be before the random.randints + # arguments because they will use up the original seed (if a custom seed is used) + random_channel = self.__get_random_channel() # Add color channel offset if checked true - self.__color_offset(randint(-doubled_glitch_amount, doubled_glitch_amount), - randint(-doubled_glitch_amount, doubled_glitch_amount), - self.__get_random_channel()) + self.__color_offset(random.randint(-doubled_glitch_amount, doubled_glitch_amount), + random.randint(-doubled_glitch_amount, + doubled_glitch_amount), + random_channel) if scan_lines: # Add scan lines if checked true @@ -360,7 +428,7 @@ def __add_scan_lines(self): # Alpha is left untouched (if present) self.outputarr[::2, :, :3] = [0, 0, 0] - def __glitch_left(self, offset): + def __glitch_left(self, offset: int): """ Grabs a rectange from inputarr and shifts it leftwards Any lost pixel data is wrapped back to the right @@ -380,8 +448,8 @@ def __glitch_left(self, offset): That's the end result! """ # Setting up values that will determine the rectangle height - start_y = randint(0, self.img_height) - chunk_height = randint(1, int(self.img_height / 4)) + start_y = random.randint(0, self.img_height) + chunk_height = random.randint(1, int(self.img_height / 4)) chunk_height = min(chunk_height, self.img_height - start_y) stop_y = start_y + chunk_height @@ -395,7 +463,7 @@ def __glitch_left(self, offset): self.outputarr[start_y:stop_y, :stop_x] = left_chunk self.outputarr[start_y:stop_y, stop_x:] = wrap_chunk - def __glitch_right(self, offset): + def __glitch_right(self, offset: int): """ Grabs a rectange from inputarr and shifts it rightwards Any lost pixel data is wrapped back to the left @@ -416,8 +484,8 @@ def __glitch_right(self, offset): That's the end result! """ # Setting up values that will determine the rectangle height - start_y = randint(0, self.img_height) - chunk_height = randint(1, int(self.img_height / 4)) + start_y = random.randint(0, self.img_height) + chunk_height = random.randint(1, int(self.img_height / 4)) chunk_height = min(chunk_height, self.img_height - start_y) stop_y = start_y + chunk_height @@ -431,14 +499,14 @@ def __glitch_right(self, offset): self.outputarr[start_y:stop_y, start_x:] = right_chunk self.outputarr[start_y:stop_y, :start_x] = wrap_chunk - def __color_offset(self, offset_x, offset_y, channel_index): + def __color_offset(self, offset_x: int, offset_y: int, channel_index: int): """ Takes the given channel's color value from inputarr, starting from (0, 0) and puts it in the same channel's slot in outputarr, starting from (offset_y, offset_x) """ - # Make sure offset_x isn't negative in the actual algo + # Make sure offset_x isn't negative in the actual algo offset_x = offset_x if offset_x >= 0 else self.img_width + offset_x offset_y = offset_y if offset_y >= 0 else self.img_height + offset_y @@ -473,7 +541,18 @@ def __color_offset(self, offset_x, offset_y, channel_index): :, channel_index] - def __get_random_channel(self): + def __get_random_channel(self) -> int: # Returns a random index from 0 to pixel_tuple_len # For an RGB image, a 0th index represents the RED channel - return randint(0, self.pixel_tuple_len - 1) + + return random.randint(0, self.pixel_tuple_len - 1) + + def __reset_rng_seed(self, offset: int = 0): + """ + Calls random.seed() with self.seed variable + + offset is for looping and getting new positions for each iteration that cointains the + previous one, otherwise we would get the same position on every loop and different + results afterwards on non fixed size loops + """ + random.seed(self.seed + offset) diff --git a/setup.py b/setup.py index a87209f..55029dd 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ from setuptools import setup, find_packages -with open('README.md', 'r') as file: - readme = file.read() +with open('README.md', 'r') as readmefile: + readme = readmefile.read() setup( name='glitch_this', - version='0.1.5', + version='1.0.0', author='TotallyNotChase', - author_email='44284917+TotallyNotChase@users.noreply.github.com', + author_email='totallynotchase42@gmail.com', description='A package to glitch images and GIFs, with highly customizable options!', long_description=readme, long_description_content_type='text/markdown', diff --git a/test.gif b/test.gif new file mode 100644 index 0000000..8219442 Binary files /dev/null and b/test.gif differ diff --git a/test_script.py b/test_script.py index 741ef0b..2255c88 100644 --- a/test_script.py +++ b/test_script.py @@ -1,7 +1,11 @@ -import os, shutil +import os +import shutil +from decimal import getcontext, Decimal from random import choice from time import time + from PIL import Image + from glitch_this import ImageGlitcher """ @@ -15,9 +19,8 @@ # Set up glitch_amount values that will be used # Float numbers from 1 to 10, upto a single decimal precision -from decimal import getcontext, Decimal -# Setting floating point precision to 1 (after decimal point) +# Setting floating point precision to 1 (after decimal point) getcontext().prec = 2 amount_list = [] start = Decimal(0.1) @@ -25,10 +28,11 @@ amount_list.append(float(start)) start += Decimal(0.1) + def test_loop(): # A method to stress test count = 0 - sum = 0 + timesum = 0 try: with open('Collections/imglog.txt', 'w') as logtxt: while(1): @@ -43,16 +47,17 @@ def test_loop(): Check DOCS for more info! """ - glitch_img = glitcher.glitch_image('test.{}'.format(fmt), level) + glitch_img = glitcher.glitch_image(f'test.{fmt}', level) # You can then save it or do anything else you want with it - glitch_img.save('Collections/glitched_test_{}.{}'.format(str(count), fmt)) + glitch_img.save(f'Collections/glitched_test_{count}.{fmt}') t1 = time() - logtxt.write('img_num: {}, level: {}\n'.format(count, level)) + logtxt.write(f'img_num: {count}, level: {level}\n') count += 1 - sum += (t1 - t0) - print('Time taken: ' + str(t1 - t0)) + timesum += (t1 - t0) + print(f'Time taken: {t1 - t0}') except KeyboardInterrupt: - print('Average time: ' + str(sum / count)) + print(f'Average time: {timesum / count}') + def test_image_to_image(): """ @@ -63,26 +68,32 @@ def test_image_to_image(): """ # All default params(i.e color_offset = False, scan_lines = False) - glitch_img = glitcher.glitch_image('test.{}'.format(fmt), 2) - glitch_img.save('Collections/glitched_test_default.{}'.format(fmt)) + glitch_img = glitcher.glitch_image(f'test.{fmt}', 2) + glitch_img.save(f'Collections/glitched_test_default.{fmt}') # Now try with scan_lines set to true - glitch_img = glitcher.glitch_image('test.{}'.format(fmt), 2, scan_lines=True) - glitch_img.save('Collections/glitched_test_scan.{}'.format(fmt)) + glitch_img = glitcher.glitch_image(f'test.{fmt}', 2, scan_lines=True) + glitch_img.save(f'Collections/glitched_test_scan.{fmt}') # Now try with color_offset set to true - glitch_img = glitcher.glitch_image('test.{}'.format(fmt), 2, color_offset=True) - glitch_img.save('Collections/glitched_test_color.{}'.format(fmt)) + glitch_img = glitcher.glitch_image(f'test.{fmt}', 2, color_offset=True) + glitch_img.save(f'Collections/glitched_test_color.{fmt}') + + # Now try glitching with a seed + # This will base the RNG used within the glitching on given seed + glitch_img = glitcher.glitch_image(f'test.{fmt}', 2, seed=42) + glitch_img.save(f'Collections/glitched_test_seed.{fmt}') # How about all of them? - glitch_img = glitcher.glitch_image('test.{}'.format(fmt), 2, color_offset=True, scan_lines=True) - glitch_img.save('Collections/glitched_test_all.{}'.format(fmt)) + glitch_img = glitcher.glitch_image(f'test.{fmt}', 2, color_offset=True, scan_lines=True, seed=42) + glitch_img.save(f'Collections/glitched_test_all.{fmt}') # You can also pass an Image object inplace of the path # Applicable in all of the examples above - img = Image.open('test.{}'.format(fmt)) - glitch_img = glitcher.glitch_image(img, 2, color_offset=True, scan_lines=True) - glitch_img.save('Collections/glitched_test_all_obj.{}'.format(fmt)) + img = Image.open(f'test.{fmt}') + glitch_img = glitcher.glitch_image(img, 2, color_offset=True, scan_lines=True, seed=42) + glitch_img.save(f'Collections/glitched_test_all_obj.{fmt}') + def test_image_to_gif(): """ @@ -100,18 +111,19 @@ def test_image_to_gif(): DURATION = 200 # Set this to however many centiseconds each frame should be visible for LOOP = 0 # Set this to how many times the gif should loop - # LOOP = 0 means infinite loop + # LOOP = 0 means infinite loop # All default params (i.e step = 1, glitch_change = 0, cycle = False, Frames = 23, color_offset = False, scan_lines = False) - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, gif=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, gif=True) glitch_imgs[0].save('Collections/glitched_test_default.gif', format='GIF', append_images=glitch_imgs[1:], save_all=True, duration=DURATION, loop=LOOP) + # Now try with scan_lines set to true - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, gif=True, scan_lines=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, gif=True, scan_lines=True) glitch_imgs[0].save('Collections/glitched_test_scan.gif', format='GIF', append_images=glitch_imgs[1:], @@ -120,7 +132,7 @@ def test_image_to_gif(): loop=LOOP) # Now try with color_offset set to true - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, gif=True, color_offset=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, gif=True, color_offset=True) glitch_imgs[0].save('Collections/glitched_test_color.gif', format='GIF', append_images=glitch_imgs[1:], @@ -129,7 +141,7 @@ def test_image_to_gif(): loop=LOOP) # Now try with 10 frames - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, gif=True, frames=10) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, gif=True, frames=10) glitch_imgs[0].save('Collections/glitched_test_frames.gif', format='GIF', append_images=glitch_imgs[1:], @@ -141,7 +153,7 @@ def test_image_to_gif(): # glitch_amount will reach glitch_max after (glitch_max - glitch_amount)/glitch_change glitches # in this case that's 8 # It'll just stay at glitch_max for the remaining duration since cycle = False - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, glitch_change=1, gif=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, glitch_change=1, gif=True) glitch_imgs[0].save('Collections/glitched_test_increment.gif', format='GIF', append_images=glitch_imgs[1:], @@ -153,7 +165,7 @@ def test_image_to_gif(): # glitch_amount will reach glitch_max after (glitch_max - glitch_amount)/glitch_change glitches # in this case that's 8 # It'll cycle back to glitch_min after that and keep incrementing by glitch_change again - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, glitch_change=1, cycle=True, gif=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, glitch_change=1, cycle=True, gif=True) glitch_imgs[0].save('Collections/glitched_test_increment_cycle.gif', format='GIF', append_images=glitch_imgs[1:], @@ -166,7 +178,7 @@ def test_image_to_gif(): # in this case that's 1 # It'll cycle back to glitch_max after that and keep incrementing (actually decrementing, in this case) # by glitch_change again - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, glitch_change=-1, cycle=True, gif=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, glitch_change=-1, cycle=True, gif=True) glitch_imgs[0].save('Collections/glitched_test_decrement_cycle.gif', format='GIF', append_images=glitch_imgs[1:], @@ -177,7 +189,7 @@ def test_image_to_gif(): # Now try with glitching only every 2nd frame # There will still be the specified number of frames (23 in this case) # But only every 2nd of the frames will be glitched - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, step=2, gif=True) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, step=2, gif=True) glitch_imgs[0].save('Collections/glitched_test_step.gif', format='GIF', append_images=glitch_imgs[1:], @@ -186,7 +198,8 @@ def test_image_to_gif(): loop=LOOP) # How about all of the above? - glitch_imgs = glitcher.glitch_image('test.{}'.format(fmt), 2, glitch_change=-1, cycle=True, gif=True, scan_lines=True, color_offset=True, frames=10, step=2) + glitch_imgs = glitcher.glitch_image(f'test.{fmt}', 2, glitch_change=-1, + cycle=True, gif=True, scan_lines=True, color_offset=True, frames=10, step=2) glitch_imgs[0].save('Collections/glitched_test_all.gif', format='GIF', append_images=glitch_imgs[1:], @@ -196,8 +209,9 @@ def test_image_to_gif(): # You can also pass an Image object inplace of the path # Applicable in all of the examples above - img = Image.open('test.{}'.format(fmt)) - glitch_imgs = glitcher.glitch_image(img, 2, gif=True, scan_lines=True, color_offset=True, frames=10) + img = Image.open(f'test.{fmt}') + glitch_imgs = glitcher.glitch_image(img, 2, glitch_change=-1, + cycle=True, gif=True, scan_lines=True, color_offset=True, frames=10, step=2) glitch_imgs[0].save('Collections/glitched_test_all_obj.gif', format='GIF', append_images=glitch_imgs[1:], @@ -205,6 +219,7 @@ def test_image_to_gif(): duration=DURATION, loop=LOOP) + def test_gif_to_gif(): """ Example of getting a glitched GIF (from another GIF) @@ -222,7 +237,7 @@ def test_gif_to_gif(): DURATION = 200 # Set this to however many centiseconds each frame should be visible for LOOP = 0 # Set this to how many times the gif should loop - # LOOP = 0 means infinite loop + # LOOP = 0 means infinite loop # All default params (i.e step = 1, glitch_change = 0, cycle = False, color_offset = False, scan_lines = False) glitch_imgs, src_duration, src_frames = glitcher.glitch_gif('test.gif', 2) @@ -298,8 +313,19 @@ def test_gif_to_gif(): duration=DURATION, loop=LOOP) + # Now try glitching with a seed + # This will base the RNG used within the glitching on given seed + glitch_imgs, src_duration, src_frames = glitcher.glitch_gif('test.gif', 2, seed=42) + glitch_imgs[0].save('Collections/glitched_gif_seed.gif', + format='GIF', + append_images=glitch_imgs[1:], + save_all=True, + duration=DURATION, + loop=LOOP) + # How about all of the above? - glitch_imgs, src_duration, src_frames = glitcher.glitch_gif('test.gif', 2, glitch_change=-1, cycle=True, scan_lines=True, color_offset=True, step=2) + glitch_imgs, src_duration, src_frames = glitcher.glitch_gif( + 'test.gif', 2, glitch_change=-1, cycle=True, scan_lines=True, color_offset=True, step=2, seed=42) glitch_imgs[0].save('Collections/glitched_gif_all.gif', format='GIF', append_images=glitch_imgs[1:], @@ -310,48 +336,45 @@ def test_gif_to_gif(): # You can also pass an Image object inplace of the path # Applicable in all of the examples above img = Image.open('test.gif') - glitch_imgs, src_duration, src_frames = glitcher.glitch_gif(img, 2, scan_lines=True, color_offset=True) + glitch_imgs, src_duration, src_frames = glitcher.glitch_gif( + img, 2, glitch_change=-1, cycle=True, scan_lines=True, color_offset=True, step=2, seed=42) glitch_imgs[0].save('Collections/glitched_test_all_obj.gif', format='GIF', append_images=glitch_imgs[1:], save_all=True, duration=DURATION, loop=LOOP) - -if __name__=='__main__': + + +if __name__ == '__main__': # Create the ImageGlitcher object glitcher = ImageGlitcher() if os.path.isdir('Collections'): shutil.rmtree('Collections') os.mkdir('Collections') - - """ - NOTE: GIF to GIF glitching is disabled by default - Reason 1: This is a time consuming process - Reason 2: No test.gif is supplied with the src (yet) - - The examples given in the method however are pre-tested - and perfectly valid! - """ - - #print('Testing GIF to GIF glitching....') - #test_gif_to_gif() - #print('Done!') - # Start Testing + # Set format of test image to png (file being used is test.png) fmt = 'png' + print('Testing GIF to GIF glitching....') + t0 = time() + test_gif_to_gif() + t1 = time() + print(f'Done! Time taken: {t1 - t0}') + print('Testing image to image glitching....') t0 = time() test_image_to_image() t1 = time() - print('Done! Time taken: ', str(t1-t0)) + print(f'Done! Time taken: {t1 - t0}') + print('Testing image to GIF glitching....') t0 = time() test_image_to_gif() t1 = time() - print('Done! Time taken: ', str(t1-t0)) + print(f'Done! Time taken: {t1 - t0}') + print('Testing infinite stress test.....\nNOTE: Use ctrl+c to stop the test') test_loop() print('Done!')