├── requirements.txt ├── MANIFEST.in ├── LICENSE ├── README.md ├── glitchart ├── __init__.py └── glitchart.py ├── .gitignore └── setup.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow>=5.3.0 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | ## Include 2 | include LICENSE requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dan Tès 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GlitchArt 2 | 3 | > Media Glitch Library for Python 4 | 5 |
6 | 7 | 8 | 9 |
10 | 11 | **GlitchArt** is a Python library that applies a glitch effect to image and video files. 12 | It does so by corrupting JPEG frames on random bytes, without screwing up files. 13 | Supported media formats: **JPEG**, **PNG**, **WebP**, **MP4**. 14 | 15 | ## Requirements 16 | 17 | - `Pillow`, which is automatically installed. 18 | - Videos require `ffmpeg` and `ffprobe` available in PATH. 19 | 20 | ## Installing 21 | 22 | ``` shell 23 | $ pip3 install glitchart 24 | ``` 25 | 26 | ## Usage 27 | 28 | ``` python 29 | import glitchart 30 | 31 | glitchart.jpeg("starrynight.jpg") 32 | ``` 33 | 34 | ## Documentation 35 | 36 | Read the source code for now, or use Python's `help()` built-in function. E.g.: 37 | 38 | ```python 39 | >>> import glitchart 40 | >>> help(glitchart.jpeg) 41 | ... 42 | ``` 43 | 44 | ## License 45 | 46 | MIT © 2019-2020 [Dan](https://github.com/delivrance) 47 | -------------------------------------------------------------------------------- /glitchart/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2019 Dan Tès 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .glitchart import jpeg, jpeg_async, png, png_async, webp, webp_async, mp4, mp4_async 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm stuff 2 | .idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .dmypy.json 116 | dmypy.json 117 | 118 | # Pyre type checker 119 | .pyre/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2019 Dan Tès 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from setuptools import setup 24 | 25 | 26 | def read(file: str) -> list: 27 | with open(file, encoding="utf-8") as r: 28 | return [i.strip() for i in r] 29 | 30 | 31 | setup( 32 | name="GlitchArt", 33 | version="1.0.0", 34 | description="Media Glitch Library for Python", 35 | url="https://github.com/delivrance/glitchart", 36 | author="Dan Tès", 37 | license="MIT", 38 | classifiers=[ 39 | "Development Status :: 5 - Production/Stable", 40 | "Intended Audience :: Developers", 41 | "License :: OSI Approved :: MIT License", 42 | "Operating System :: OS Independent", 43 | "Programming Language :: Python", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.4", 46 | "Programming Language :: Python :: 3.5", 47 | "Programming Language :: Python :: 3.6", 48 | "Programming Language :: Python :: 3.7", 49 | "Programming Language :: Python :: 3.8", 50 | "Programming Language :: Python :: 3 :: Only", 51 | "Programming Language :: Python :: Implementation", 52 | "Programming Language :: Python :: Implementation :: CPython", 53 | "Programming Language :: Python :: Implementation :: PyPy", 54 | "Topic :: Artistic Software", 55 | "Topic :: Multimedia", 56 | "Topic :: Multimedia :: Graphics", 57 | "Topic :: Multimedia :: Graphics :: Graphics Conversion", 58 | "Topic :: Multimedia :: Video", 59 | "Topic :: Multimedia :: Video :: Conversion" 60 | ], 61 | keywords="media image video glitch library python jpeg jpg png webp mp4", 62 | project_urls={ 63 | "Tracker": "https://github.com/delivrance/glitchart/issues", 64 | "Source": "https://github.com/delivrance/glitchart" 65 | }, 66 | python_requires="~=3.4", 67 | packages=["glitchart"], 68 | zip_safe=False, 69 | install_requires=read("requirements.txt") 70 | ) 71 | -------------------------------------------------------------------------------- /glitchart/glitchart.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2019 Dan Tès 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import asyncio 24 | import logging 25 | import os 26 | import subprocess 27 | from pathlib import Path 28 | from random import Random 29 | from uuid import uuid4 30 | 31 | from PIL import Image 32 | 33 | MIN_AMOUNT_IMAGE = 1 34 | MAX_AMOUNT_IMAGE = 10 35 | 36 | MIN_AMOUNT_VIDEO = 0 37 | MAX_AMOUNT_VIDEO = 3 38 | 39 | MIN_SEED = -2 ** 63 40 | MAX_SEED = 2 ** 63 - 1 41 | 42 | SOS = b"\xFF\xDA" # Start Of Scan 43 | EOI = b"\xFF\xD9" # End Of Image 44 | 45 | OUT_NAME_TEMPLATE = "{}_glitch.{}" 46 | 47 | log = logging.getLogger(__name__) 48 | 49 | 50 | def jpeg(photo: str, 51 | seed: int = None, 52 | min_amount: int = MIN_AMOUNT_IMAGE, 53 | max_amount: int = MAX_AMOUNT_IMAGE, 54 | inplace: bool = False) -> str: 55 | """Glitch a JPEG file. A new image will be saved in the current working directory with the string 56 | "_glitch" appended to the filename. E.g.: "monalisa.jpg" becomes "monalisa_glitch.jpg". 57 | 58 | Args: 59 | photo (str): 60 | JPEG photo file to glitch. 61 | Pass a file path as string to glitch a photo that exists on your local machine. 62 | 63 | seed (int, optional): 64 | Pseudo-random number generator seed. 65 | Using again the same seed on the original file will result in identical glitched images. 66 | Defaults to a random value. 67 | 68 | min_amount (int, optional): 69 | Minimum amount of bytes to corrupt. 70 | A negative value will result in min_amount = 0. 71 | A value higher than max_amount will result in max_amount = min_amount. 72 | The actual amount will be chosen randomly in range [min_amount, max_amount]. 73 | Defaults to 1. 74 | 75 | max_amount (int, optional): 76 | Maximum amount of bytes to corrupt. 77 | A negative value will result in max_amount = 1. 78 | A value lower than min_amount will result in max_amount = min_amount. 79 | The actual amount will be chosen randomly in range [min_amount, max_amount]. 80 | Defaults to 10. 81 | 82 | inplace (bool, optional): 83 | Pass True to glitch the image in-place and avoid creating a new JPEG file. 84 | This will overwrite the original image. 85 | Defaults to False. 86 | 87 | Returns: 88 | On success, the absolute path of the glitched image is returned. 89 | """ 90 | out = photo if inplace else OUT_NAME_TEMPLATE.format(Path(photo).stem, "jpg") 91 | prng = Random(seed) 92 | 93 | if min_amount < 0: 94 | min_amount = 0 95 | 96 | if max_amount < 0: 97 | max_amount = 1 98 | 99 | if min_amount > max_amount: 100 | max_amount = min_amount 101 | 102 | amount = prng.randint(min_amount, max_amount) 103 | 104 | with open(photo, "rb") as f: 105 | original = f.read() 106 | 107 | start = original.index(SOS) + len(SOS) + 10 108 | end = original.rindex(EOI) 109 | 110 | data = bytearray(original[start:end]) 111 | glitched = set() 112 | 113 | for _ in range(amount): 114 | while True: 115 | index = prng.randrange(len(data)) 116 | 117 | if index not in glitched: 118 | if data[index] not in [0, 255]: 119 | glitched.add(index) 120 | break 121 | 122 | while True: 123 | value = prng.randint(1, 254) 124 | 125 | if data[index] != value: 126 | data[index] = value 127 | break 128 | 129 | with open(out, "wb") as f: 130 | f.write( 131 | original[:start] 132 | + data 133 | + original[end:] 134 | ) 135 | 136 | return Path(out).absolute() 137 | 138 | 139 | async def jpeg_async(*args, **kwargs): 140 | return jpeg(*args, **kwargs) 141 | 142 | 143 | def png(photo: str, 144 | seed: int = None, 145 | min_amount: int = MIN_AMOUNT_IMAGE, 146 | max_amount: int = MAX_AMOUNT_IMAGE, 147 | inplace: bool = False): 148 | out = photo if inplace else OUT_NAME_TEMPLATE.format(Path(photo).stem, "png") 149 | jpg_path = "{}.jpg".format(uuid4()) 150 | 151 | png = Image.open(photo).convert("RGBA") 152 | 153 | bg = Image.new("RGB", png.size, (255, 255, 255)) 154 | bg.paste(png, png) 155 | bg.save(jpg_path) 156 | 157 | jpeg(jpg_path, seed, min_amount, max_amount, True) 158 | 159 | Image.open(jpg_path).convert("RGBA").save(out) 160 | 161 | os.remove(jpg_path) 162 | 163 | return Path(out).absolute() 164 | 165 | 166 | async def png_async(*args, **kwargs): 167 | return png(*args, **kwargs) 168 | 169 | 170 | def webp(photo: str, 171 | seed: int = None, 172 | min_amount: int = MIN_AMOUNT_IMAGE, 173 | max_amount: int = MAX_AMOUNT_IMAGE, 174 | inplace: bool = False): 175 | out = photo if inplace else OUT_NAME_TEMPLATE.format(Path(photo).stem, "webp") 176 | png_path = "{}.png".format(uuid4()) 177 | 178 | webp = Image.open(photo) 179 | webp.save(png_path) 180 | 181 | png(png_path, seed, min_amount, max_amount, True) 182 | 183 | Image.open(png_path).save(out) 184 | 185 | os.remove(png_path) 186 | 187 | return Path(out).absolute() 188 | 189 | 190 | async def webp_async(*args, **kwargs): 191 | return webp(*args, **kwargs) 192 | 193 | 194 | def mp4(video: str, 195 | seed: int = None, 196 | min_amount: int = MIN_AMOUNT_VIDEO, 197 | max_amount: int = MAX_AMOUNT_VIDEO, 198 | inplace: bool = False): 199 | out = video if inplace else OUT_NAME_TEMPLATE.format(Path(video).stem, "mp4") 200 | uuid = uuid4() 201 | 202 | try: 203 | fps = subprocess.check_output( 204 | "ffprobe -v error -select_streams v -of " 205 | "default=noprint_wrappers=1:nokey=1 -show_entries stream=r_frame_rate {video}".format( 206 | video=video 207 | ), 208 | shell=True 209 | ).strip().decode() 210 | 211 | os.system( 212 | "ffmpeg -loglevel quiet -i {video} {uuid}_%8d.jpg".format( 213 | video=video, 214 | uuid=uuid 215 | ) 216 | ) 217 | 218 | prng = Random(seed) 219 | 220 | for p in sorted(Path().rglob(f"{uuid}_*.jpg")): 221 | jpeg(str(p), prng.getrandbits(2500), min_amount, max_amount, inplace=True) 222 | 223 | os.system( 224 | "ffmpeg -loglevel quiet -r {fps} -i {uuid}_%8d.jpg {out} -y".format( 225 | fps=fps, 226 | uuid=uuid, 227 | out=out 228 | ) 229 | ) 230 | except Exception as e: 231 | log.error(e) 232 | finally: 233 | for p in Path().rglob(f"{uuid}_*.jpg"): 234 | try: 235 | os.remove(str(p)) 236 | except OSError: 237 | pass 238 | 239 | return Path(out).absolute() 240 | 241 | 242 | async def mp4_async(video: str, 243 | seed: int = None, 244 | min_amount: int = MIN_AMOUNT_VIDEO, 245 | max_amount: int = MAX_AMOUNT_VIDEO, 246 | inplace: bool = False): 247 | out = video if inplace else OUT_NAME_TEMPLATE.format(Path(video).stem, "mp4") 248 | uuid = uuid4() 249 | 250 | try: 251 | fps = subprocess.check_output( 252 | "ffprobe -v error -select_streams v -of " 253 | "default=noprint_wrappers=1:nokey=1 -show_entries stream=r_frame_rate {video}".format( 254 | video=video 255 | ), 256 | shell=True 257 | ).strip().decode() 258 | 259 | process = await asyncio.create_subprocess_shell( 260 | "ffmpeg -loglevel quiet -i {video} {uuid}_%8d.jpg".format( 261 | video=video, 262 | uuid=uuid 263 | ) 264 | ) 265 | await process.wait() 266 | 267 | prng = Random(seed) 268 | 269 | for p in sorted(Path().rglob(f"{uuid}_*.jpg")): 270 | jpeg(str(p), prng.randint(MIN_SEED, MAX_SEED), min_amount, max_amount, inplace=True) 271 | 272 | process = await asyncio.create_subprocess_shell( 273 | "ffmpeg -loglevel quiet -r {fps} -i {uuid}_%8d.jpg {out} -y".format( 274 | fps=fps, 275 | uuid=uuid, 276 | out=out 277 | ) 278 | ) 279 | await process.wait() 280 | except Exception as e: 281 | log.error(e) 282 | finally: 283 | for p in Path().rglob("{uuid}_*.jpg".format(uuid=uuid)): 284 | try: 285 | os.remove(str(p)) 286 | except OSError: 287 | pass 288 | 289 | return Path(out).absolute() 290 | --------------------------------------------------------------------------------