├── .gitattributes ├── .gitignore ├── README.md ├── img ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── icon.bmp ├── po2ir-installer.exe ├── po2resizer.ico └── po2resizer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # dotenv 78 | .env 79 | 80 | # virtualenv 81 | venv/ 82 | ENV/ 83 | 84 | # Spyder project settings 85 | .spyderproject 86 | 87 | # Rope project settings 88 | .ropeproject 89 | 90 | # ========================= 91 | # Operating System Files 92 | # ========================= 93 | 94 | # OSX 95 | # ========================= 96 | 97 | .DS_Store 98 | .AppleDouble 99 | .LSOverride 100 | 101 | # Thumbnails 102 | ._* 103 | 104 | # Files that might appear in the root of a volume 105 | .DocumentRevisions-V100 106 | .fseventsd 107 | .Spotlight-V100 108 | .TemporaryItems 109 | .Trashes 110 | .VolumeIcon.icns 111 | 112 | # Directories potentially created on remote AFP share 113 | .AppleDB 114 | .AppleDesktop 115 | Network Trash Folder 116 | Temporary Items 117 | .apdisk 118 | 119 | # Windows 120 | # ========================= 121 | 122 | # Windows image file caches 123 | Thumbs.db 124 | ehthumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | # Recycle Bin used on file shares 130 | $RECYCLE.BIN/ 131 | 132 | # Windows Installer files 133 | *.cab 134 | *.msi 135 | *.msm 136 | *.msp 137 | 138 | # Windows shortcuts 139 | *.lnk 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Power Of 2 Image Resizer -
Optimize Game Engine Textures 3 |

4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | 8 | **About** 9 | --- 10 | 11 | **CURRENTLY WINDOWS ONLY** - *you can still use the python script on any OS if you have the Python and the dependencies installed (pyGame and Pillow), but there is no shell extension, installer, or binary.* 12 | 13 | This program resizes images to a power of 2 `(256, 512, 1024, 2048, etc.)`. Many game engines cannot use textures without them being a power of 2. Some game engines will use them with poor optimization and limited features. For example, in Unreal Engine, textures that are not a power of 2 cannot use texture streaming and will appear horribly jarring from a distance. 14 | This program will take an image, find the closest power of 2 (using a user-defined threshold), and resize that image. So an image that is 1080x1200 would be resized (stretched in this case - images are never cut or cropped) to 1024x1024. Then they are ready to be used in a game engine. 15 | 16 | As images are not cut or cropped, seamless textures will still be seamless, so no worries. [Turn the compression down to 0 in the config if you are worried about lossiness](#configuration). That will make these resizes lossless! 17 | 18 | I made this program for my own use, as when working on game projects, I stumbled across thousands of wonderful free textures. The problem was that basically none of them were power of 2. Some would be 1000x1000 or 1200x1200 for example. So I found myself constantly opening bulky image editing programs, navigating through menus, finding the image resizing tool, manually typing in the new dimensions, and saving it. The whole process taking around 10 clicks and 30 seconds per file (if I was going fast). Going through hundreds of these, let alone thousands, is tedious, mind and finger numbing, and time-consuming. This takes care of everything for you *automatically!* 19 | 20 | **Supported Image Types:** 21 | * JPG 22 | * PNG 23 | * GIF 24 | * BMP 25 | * TGA* 26 | 27 | *Targa files cannot be compressed. 28 | 29 | There are more file types supported (anything supported by Pillow) like ICO, but they really won't be used for textures so I won't bother listing them. 30 | 31 | This program is written in Python and uses [Pillow](https://github.com/python-pillow/Pillow) and [pyGame](https://www.pygame.org/docs/) (for Targa support). 32 | 33 | **How to Use:** 34 | --- 35 | 36 | After installing, simply right click on an image file or *multiple* selected image files and click "Resize Image(s) to Power of 2". **Easy!** 37 | 38 |

39 | 40 |

41 | 42 |
43 | 44 |

45 | 46 |

47 | 48 |
49 | 50 | It will resize all selected images to the closest power of 2. 51 | 52 | **You can also use it from the command line by using any number of images as the parameters.** 53 | 54 |

55 | 56 |

57 | 58 | 59 | **Installation** 60 | --- 61 | ***Installing:*** 62 | 63 | Just run the po2ir-installer.exe. It contains all dependencies and will be installed in your Program Files in a folder named "po2ir". 64 | The installer will add the following windows registry keys (needed for right-click context menu resizing): 65 | 66 | * `HKEY_CLASSES_ROOT\SystemFileAssociations\image\shell\Resize Image(s) to Power of 2` 67 | * `MultiSelectModel = Player (Needed for multiple selected context menu)` 68 | * `HKEY_CLASSES_ROOT\SystemFileAssociations\image\shell\Resize Image(s) to Power of 2\command` 69 | * `HKEY_CLASSES_ROOT\SystemFileAssociations\.tga\shell\Resize Image(s) to Power of 2` 70 | * `MultiSelectModel = Player (Needed for multiple selected context menu)` 71 | * `HKEY_CLASSES_ROOT\SystemFileAssociations\.tga\shell\Resize Image(s) to Power of 2\command` 72 | 73 | ***Uninstalling:*** 74 | 75 | Run the uninstaller exe in the po2ir folder. The uninstaller will also delete the registry keys. 76 | 77 | **Configuration** 78 | --- 79 | There will be a file called "config.txt" in the program folder ("C:/Program Files (x86)/po2ir/config.txt" on my own computer). You can edit 2 variables in there to control how the program behaves. There are also instructions in that file, which I will paste below: 80 | 81 | > Threshold is how close an image has to be to a smaller power of 2 before it will be scaled down. Range from 0.0 to 1.0 82 | > 83 | >For instance, if I have an image that is 1025 pixels, I don't want to scale that up to 2048! I want it to lose one pixel and be 1024. But if I have an image that is 1600 pixels, perhaps I do want to scale up rather than down (because that would be a large loss of quality). Threshold is the percentage of how close to the smaller size it has to be. Default is 0.25. This means that if I had an image of size 1280, it would be scaled down to 1024 (1024 + (1024 * 0.25) = 1280). Anything above that would be scaled to 2048. If the threshold was 0.5, then anything below 1536 would be scaled down (1024 + (1024 * 0.5) = 1536). So 1.0 would be ALWAYS SCALE DOWN and 0.0 would be ALWAYS SCALE UP. 84 | > 85 | > Compression is just the level of compression to apply. PIL's default is 6. The default here is 5. 0 is no compression at all (although metadata will still say the file is compressed, I promise it's not!). Compression does not apply to TGA files, as they are always saved uncompressed. Range is from 0-9 86 | 87 | **License** 88 | --- 89 | MIT License 90 | 91 | Copyright (c) 2017 Ryan Andrew Walters 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy 94 | of this software and associated documentation files (the "Software"), to deal 95 | in the Software without restriction, including without limitation the rights 96 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 97 | copies of the Software, and to permit persons to whom the Software is 98 | furnished to do so, subject to the following conditions: 99 | 100 | The above copyright notice and this permission notice shall be included in all 101 | copies or substantial portions of the Software. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 106 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 108 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 109 | SOFTWARE. 110 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/3.png -------------------------------------------------------------------------------- /img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/4.png -------------------------------------------------------------------------------- /img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/5.png -------------------------------------------------------------------------------- /img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/6.png -------------------------------------------------------------------------------- /img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/7.png -------------------------------------------------------------------------------- /img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/8.png -------------------------------------------------------------------------------- /img/icon.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/img/icon.bmp -------------------------------------------------------------------------------- /po2ir-installer.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/po2ir-installer.exe -------------------------------------------------------------------------------- /po2resizer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanAWalters/PowerOf2ImageResizer/d36a0fd28260bcf927d99fce1587e5c8c2364862/po2resizer.ico -------------------------------------------------------------------------------- /po2resizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Ryan Walters 3 | 4 | This program resizes images to power of 2 for use in game engines. 5 | """ 6 | 7 | from __future__ import print_function 8 | import sys 9 | # import warnings 10 | from PIL import Image 11 | from PIL import TgaImagePlugin # required import for cxfreeze to correctly package 12 | from pygame import image 13 | 14 | sizes = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192] # po2 sizes 15 | 16 | 17 | def get_closest(y): 18 | """ Return the closest power of 2 in either direction""" 19 | return min(sizes, key=lambda x: abs(x - y)) 20 | 21 | 22 | def po2(im, threshold=0.25): 23 | """ 24 | Return a resized image that is a power of 2 25 | (Checking that if image size is reduced it was fairly close to that size anyway based on threshold) 26 | """ 27 | width, height = im.size 28 | largest_dim = max(width, height) 29 | new_dim = max(get_closest(width), get_closest(height)) # new dimension will be largest of x or y po2 30 | 31 | if new_dim < largest_dim: # if it's smaller, make sure its within threshold 32 | if (largest_dim - new_dim) > int(new_dim * threshold): 33 | new_dim = sizes[sizes.index(new_dim) + 1] 34 | return im.resize((new_dim, new_dim), resample=Image.BICUBIC) 35 | else: 36 | return im.resize((new_dim, new_dim), resample=Image.LANCZOS) 37 | else: 38 | return im.resize((new_dim, new_dim), resample=Image.BICUBIC) 39 | 40 | 41 | def read_config(): 42 | """ Returns the threshold and compression level from the config file """ 43 | threshold = 0.25 44 | compression = 0 45 | 46 | # No need to continue parsing if vars found already 47 | found_vars = [False, False] 48 | found_var_count = 0 49 | 50 | try: 51 | with open('config.txt', 'r') as config: 52 | for line in config: 53 | 54 | if found_var_count == 2: 55 | break 56 | 57 | if not found_vars[0]: 58 | if "THRESHOLD=" in line.upper(): 59 | try: 60 | threshold = float(line.upper().replace('THRESHOLD=', '')) 61 | found_vars[0] = True 62 | continue 63 | except ValueError: 64 | print('Invalid threshold in config.txt') 65 | 66 | if not found_vars[0]: 67 | if "COMPRESSION=" in line.upper(): 68 | try: 69 | compression = int(line.upper().replace('COMPRESSION=', '')) 70 | found_vars[1] = True 71 | continue 72 | except ValueError: 73 | print('Invalid compression level in config.txt') 74 | except IOError: 75 | print("Cannot open config.txt") 76 | 77 | return threshold, compression 78 | 79 | 80 | def save_targa(im, arg): 81 | """ Convert PIL image to pygame image and save. PIL cannot save tga and no other library works with PIL """ 82 | image.save(image.fromstring(im.tobytes(), im.size, im.mode), arg.split('.', 1)[0] + '.tga') 83 | 84 | 85 | def main(): 86 | threshold, compression = read_config() 87 | try: 88 | # warnings.simplefilter('ignore', Image.DecompressionBombWarning) 89 | if len(sys.argv) > 1: 90 | for arg in sys.argv[1:]: 91 | try: 92 | im = Image.open(arg) 93 | if im.format.upper() == "TGA": 94 | save_targa(po2(im, threshold), arg) 95 | else: 96 | po2(im, threshold).save(arg, im.format.lower(), compress_level=compression) 97 | except IOError: 98 | print("IO ERROR: Is file an image? -> ", arg) 99 | except MemoryError: 100 | print("OOM") 101 | 102 | main() 103 | 104 | 105 | 106 | --------------------------------------------------------------------------------