├── .gitignore ├── LICENSE ├── README.md ├── convert_nfp.py ├── nfp.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,visualstudiocode 4 | 5 | ### PyCharm ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # AWS User-specific 17 | .idea/**/aws.xml 18 | 19 | # Generated files 20 | .idea/**/contentModel.xml 21 | 22 | # Sensitive or high-churn files 23 | .idea/**/dataSources/ 24 | .idea/**/dataSources.ids 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | .idea/**/dbnavigator.xml 30 | 31 | # Gradle 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # Gradle and Maven with auto-import 36 | # When using Gradle or Maven with auto-import, you should exclude module files, 37 | # since they will be recreated, and may cause churn. Uncomment if using 38 | # auto-import. 39 | # .idea/artifacts 40 | # .idea/compiler.xml 41 | # .idea/jarRepositories.xml 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | # *.iml 46 | # *.ipr 47 | 48 | # CMake 49 | cmake-build-*/ 50 | 51 | # Mongo Explorer plugin 52 | .idea/**/mongoSettings.xml 53 | 54 | # File-based project format 55 | *.iws 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Editor-based Rest Client 76 | .idea/httpRequests 77 | 78 | # Android studio 3.1+ serialized cache file 79 | .idea/caches/build_file_checksums.ser 80 | 81 | ### PyCharm Patch ### 82 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 83 | 84 | # *.iml 85 | # modules.xml 86 | # .idea/misc.xml 87 | # *.ipr 88 | 89 | # Sonarlint plugin 90 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 91 | .idea/**/sonarlint/ 92 | 93 | # SonarQube Plugin 94 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 95 | .idea/**/sonarIssues.xml 96 | 97 | # Markdown Navigator plugin 98 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 99 | .idea/**/markdown-navigator.xml 100 | .idea/**/markdown-navigator-enh.xml 101 | .idea/**/markdown-navigator/ 102 | 103 | # Cache file creation bug 104 | # See https://youtrack.jetbrains.com/issue/JBR-2257 105 | .idea/$CACHE_FILE$ 106 | 107 | # CodeStream plugin 108 | # https://plugins.jetbrains.com/plugin/12206-codestream 109 | .idea/codestream.xml 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | share/python-wheels/ 135 | *.egg-info/ 136 | .installed.cfg 137 | *.egg 138 | MANIFEST 139 | 140 | # PyInstaller 141 | # Usually these files are written by a python script from a template 142 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 143 | *.manifest 144 | *.spec 145 | 146 | # Installer logs 147 | pip-log.txt 148 | pip-delete-this-directory.txt 149 | 150 | # Unit test / coverage reports 151 | htmlcov/ 152 | .tox/ 153 | .nox/ 154 | .coverage 155 | .coverage.* 156 | .cache 157 | nosetests.xml 158 | coverage.xml 159 | *.cover 160 | *.py,cover 161 | .hypothesis/ 162 | .pytest_cache/ 163 | cover/ 164 | 165 | # Translations 166 | *.mo 167 | *.pot 168 | 169 | # Django stuff: 170 | *.log 171 | local_settings.py 172 | db.sqlite3 173 | db.sqlite3-journal 174 | 175 | # Flask stuff: 176 | instance/ 177 | .webassets-cache 178 | 179 | # Scrapy stuff: 180 | .scrapy 181 | 182 | # Sphinx documentation 183 | docs/_build/ 184 | 185 | # PyBuilder 186 | .pybuilder/ 187 | target/ 188 | 189 | # Jupyter Notebook 190 | .ipynb_checkpoints 191 | 192 | # IPython 193 | profile_default/ 194 | ipython_config.py 195 | 196 | # pyenv 197 | # For a library or package, you might want to ignore these files since the code is 198 | # intended to run in multiple environments; otherwise, check them in: 199 | # .python-version 200 | 201 | # pipenv 202 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 203 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 204 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 205 | # install all needed dependencies. 206 | #Pipfile.lock 207 | 208 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 209 | __pypackages__/ 210 | 211 | # Celery stuff 212 | celerybeat-schedule 213 | celerybeat.pid 214 | 215 | # SageMath parsed files 216 | *.sage.py 217 | 218 | # Environments 219 | .env 220 | .venv 221 | env/ 222 | venv/ 223 | ENV/ 224 | env.bak/ 225 | venv.bak/ 226 | 227 | # Spyder project settings 228 | .spyderproject 229 | .spyproject 230 | 231 | # Rope project settings 232 | .ropeproject 233 | 234 | # mkdocs documentation 235 | /site 236 | 237 | # mypy 238 | .mypy_cache/ 239 | .dmypy.json 240 | dmypy.json 241 | 242 | # Pyre type checker 243 | .pyre/ 244 | 245 | # pytype static type analyzer 246 | .pytype/ 247 | 248 | # Cython debug symbols 249 | cython_debug/ 250 | 251 | ### VisualStudioCode ### 252 | .vscode/* 253 | !.vscode/settings.json 254 | !.vscode/tasks.json 255 | !.vscode/launch.json 256 | !.vscode/extensions.json 257 | *.code-workspace 258 | 259 | # Local History for Visual Studio Code 260 | .history/ 261 | 262 | ### VisualStudioCode Patch ### 263 | # Ignore all local history of files 264 | .history 265 | .ionide 266 | 267 | # End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComputerCraft NFP Image Converter 2 | A Python program to convert images into NFP images for display on ComputerCraft monitors inside of Minecraft. Inspired by [this program.](https://old.reddit.com/r/feedthebeast/comments/1ee4xf/easy_image_to_computercraft_converter/c9zsevu) Feel free to open issues or PRs. 3 | 4 | The `nfp.py` module can convert between standard image formats and NFP. When converting from image to NFP, it uses [Pillow](https://python-pillow.org/) to [quantize](https://en.wikipedia.org/wiki/Quantization_(image_processing)) the image, reducing its color palette to the 16 available ComputerCraft colors. 5 | 6 | `convert_nfp.py` is a simple command-line utilty that uses this module, made because why not? 7 | 8 | ## NFP format 9 | "NFP" is a simple text-based 16-color image format used by ComputerCraft's [paint](http://www.computercraft.info/wiki/Paint) program and the [paintutils](http://www.computercraft.info/wiki/Paintutils_(API)) API. Each pixel is represented by a one-digit hex value from 0-f (decimal 0-15) corresponding to an index into the 16-color [ComputerCraft color palette](http://www.computercraft.info/wiki/Colors_(API)#Colors). Each row is terminated by a newline. 10 | 11 | A black/white 4x4 checkerboard NFP image looks like this: 12 | ```bash 13 | f0f0 14 | 0f0f 15 | f0f0 16 | 0f0f 17 | ``` 18 | 19 | ## Installation 20 | ```bash 21 | python3 -m pip install -r requirements.txt 22 | ``` 23 | 24 | ## Usage and Example 25 | Converting from image -> NFP: 26 | 27 | ```bash 28 | python3 convert_nfp.py image.png 29 | ``` 30 | (Images are automatically resized to 164x81, the maximum resolution for 8 by 6 (max size) [scale 0.5](http://www.computercraft.info/wiki/Monitor.setTextScale) ComputerCraft monitors, before conversion, unless the `--skip-resize`, `--resize-width`, or `--resize-height` arguments are specified.) 31 | 32 | Converting from NFP -> PNG: 33 | ```bash 34 | python3 convert_nfp.py image.nfp 35 | ``` 36 | 37 | Converting from NFP -> JPEG: 38 | ```bash 39 | python3 convert_nfp.py image.nfp --format=JPEG 40 | ``` 41 | 42 | Converting multiple files at once: 43 | ```bash 44 | python3 convert_nfp.py image1.nfp image2.nfp [...] 45 | ``` 46 | (Special thanks to Commandcracker for this [feature](https://github.com/DownrightNifty/computercraft-stuff/pull/3)!) 47 | 48 | See `convert_nfp.py -h` for full usage info. See the [Pillow docs](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#fully-supported-formats) for supported conversion formats. 49 | 50 | After converting an image to NFP, you can upload it to pastebin, and use [`paintutils.drawImage()`](http://www.computercraft.info/wiki/Paintutils.drawImage) to fetch and display it in-game. Example program that downloads an NFP from pastebin (like [this one](https://pastebin.com/ku2jnU6X)) using the paste ID passed in as an argument and displays it on a connected monitor: 51 | 52 | ```lua 53 | local args = {...} 54 | local paste_id = args[1] 55 | shell.run("delete image.nfp") --delete file if it already exists 56 | shell.run("pastebin", "get", paste_id, "image.nfp") --fetch NFP image from pastebin at given paste_id 57 | print("Drawing...") 58 | local old_term = term.current() 59 | local mon = peripheral.find("monitor") --find and attach to monitor 60 | mon.setTextScale(0.5) 61 | term.redirect(mon) 62 | term.clear() 63 | --draw image through paintutils 64 | local image = paintutils.loadImage("image.nfp") 65 | paintutils.drawImage(image, 0, 0) 66 | term.redirect(old_term) 67 | print("Done") 68 | ``` 69 | 70 | This example program has been [posted to pastebin](https://pastebin.com/MuZxYKrQ), so you can just use the below to display an example image: 71 | 72 | ```bash 73 | pastebin get MuZxYKrQ disp_nfp 74 | disp_nfp yjXanZ0j 75 | ``` 76 | 77 | Tested on Python 3. 78 | -------------------------------------------------------------------------------- /convert_nfp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from PIL import Image 4 | import nfp 5 | import argparse 6 | import os 7 | 8 | # default resize width/height when converting image -> nfp 9 | DEFAULT_WIDTH, DEFAULT_HEIGHT = 164, 81 10 | 11 | desc = ( 12 | "Convert standard image files to ComputerCraft nfp files, and vice " 13 | "versa. Input file type is identified by extension (.nfp, .jpg, etc.), " 14 | "and output files use the input filename with a new extension." 15 | ) 16 | files_help = "input files, nfp or image (must have correct file extension)" 17 | nfp_desc = "optional arguments when converting image -> nfp" 18 | skip_help = "skip default behavior of resizing image before conversion" 19 | width_help = "if resizing, new width (default: {})".format(DEFAULT_WIDTH) 20 | height_help = "if resizing, new height (default: {})".format(DEFAULT_HEIGHT) 21 | im_desc = "optional arguments when converting nfp -> image" 22 | format_help = ( 23 | "output format passed to Image.save() (also output file extension " 24 | "unless -e argument specified), see PIL docs for supported formats, " 25 | "default: PNG" 26 | ) 27 | ext_help = ( 28 | "if specified, will be used as the output file extension instead of " 29 | "FORMAT" 30 | ) 31 | rm_help = "remove the original image after converting" 32 | dither_help = "enables dithering" 33 | 34 | parser = argparse.ArgumentParser(description=desc) 35 | parser.add_argument("files", help=files_help, nargs='+') 36 | nfp_group = parser.add_argument_group("nfp arguments", description=nfp_desc) 37 | nfp_group.add_argument("--skip-resize", "-s", help=skip_help, 38 | action="store_true", default=False) 39 | nfp_group.add_argument("--resize-width", "-w", help=width_help, 40 | metavar="WIDTH", type=int, default=DEFAULT_WIDTH) 41 | nfp_group.add_argument("--resize-height", "-H", help=height_help, 42 | metavar="HEIGHT", type=int, default=DEFAULT_HEIGHT) 43 | im_group = parser.add_argument_group("image arguments", description=im_desc) 44 | im_group.add_argument("--format", "-f", help=format_help, metavar="FORMAT", 45 | dest="f_format", default="PNG") 46 | im_group.add_argument("--extension", "-e", help=ext_help) 47 | im_group.add_argument("--remove", "-r", help=rm_help, action="store_true") 48 | im_group.add_argument("--dither", "-d", help=dither_help, action="store_true") 49 | 50 | args = parser.parse_args() 51 | 52 | for file in args.files: 53 | filename, ext = os.path.splitext(file) 54 | if not ext: 55 | parser.error("filename must have appropriate extension") 56 | if ext.upper() == ".NFP": 57 | with open(file, "rt") as f: 58 | nfp_file = f.read() 59 | im = nfp.nfp_to_img(nfp_file) 60 | new_ext = args.f_format.replace(" ", "").lower() 61 | if args.extension: 62 | new_ext = args.extension 63 | im.save("{}.{}".format(filename, new_ext), args.f_format) 64 | else: 65 | im = Image.open(file) 66 | if args.skip_resize: 67 | nfp_file = nfp.img_to_nfp(im, dither=1 if args.dither else 0) 68 | else: 69 | nfp_file = nfp.img_to_nfp( 70 | im, 71 | (args.resize_width, args.resize_height), 72 | dither=1 if args.dither else 0 73 | ) 74 | with open("{}.nfp".format(filename), "wt") as f: 75 | f.write(nfp_file) 76 | if args.remove: 77 | os.remove(file) 78 | -------------------------------------------------------------------------------- /nfp.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | 4 | # NFP is a simple text-based 16-color image format used by ComputerCraft's paint 5 | # program. Each pixel is represented by a one-digit hex value from 0-f (decimal 6 | # 0-15) corresponding to an index into the 16-color ComputerCraft color palette. 7 | # Each row is terminated by a newline. 8 | # 9 | # Example of a 4x4 black/white checkerboard pattern NFP image: 10 | # f0f0 11 | # 0f0f 12 | # f0f0 13 | # 0f0f 14 | 15 | # values come from http://www.computercraft.info/wiki/Colors_(API) 16 | CC_COLORS = ( 17 | (240, 240, 240), # white, 0 18 | (242, 178, 51), # orange, 1 19 | (229, 127, 216), # magenta, 2 20 | (153, 178, 242), # lightBlue, 3 21 | (222, 222, 108), # yellow, 4 22 | (127, 204, 25), # lime, 5 23 | (242, 178, 204), # pink, 6 24 | (76, 76, 76), # gray, 7 25 | (153, 153, 153), # lightGray, 8 26 | (76, 153, 178), # cyan, 9 27 | (178, 102, 229), # purple, a 28 | (51, 102, 204), # blue, b 29 | (127, 102, 76), # brown, c 30 | (87, 166, 78), # green, d 31 | (204, 76, 76), # red, e 32 | (25, 25, 25) # black, f 33 | ) 34 | 35 | # Takes a PIL image and converts to nfp. Returns nfp data as string. 36 | # If new_size is provided, image will be resized before conversion. 37 | # new_size should be a 2-tuple: (width, height). 38 | # Recommended size for CC monitors at text scale 0.5 is (164, 81). 39 | def img_to_nfp(im, new_size=None, dither=0): 40 | if new_size: 41 | im = im.resize(new_size) 42 | # A technique called image quantization is used to reduce the input image's 43 | # color palette to only the 16 ComputerCraft colors. 44 | im = _quantize_with_colors(im, CC_COLORS, dither) 45 | # After quantize, im is mode "P" (palletized), so im.getdata() returns a 46 | # sequence of ints representing indexes into the image's 16-color palette 47 | # from 0-15 (hex 0-f) for each pixel in the image. This is flattened, so 48 | # the values for image line two immediately follow image line one's values. 49 | # We use np.reshape() to turn it into a 2D numpy array whose values can be 50 | # accessed through arr[row][col] notation. 51 | data = im.getdata() 52 | width, height = im.size 53 | data_2d = np.reshape(np.array(data), (height, width)) 54 | # convert from np array back to list for faster iteration 55 | data_2d = data_2d.tolist() 56 | nfp_im = "" 57 | for row in range(height): 58 | for col in range(width): 59 | # convert 0-15 decimal value to hex string (0-f) 60 | nfp_im += format(data_2d[row][col], "x") 61 | if row != len(data_2d) - 1: 62 | nfp_im += "\n" 63 | return nfp_im 64 | 65 | # Takes nfp (as string data) and returns a PIL image. 66 | def nfp_to_img(nfp): 67 | nfp = nfp.splitlines() 68 | height = len(nfp) 69 | width = len(nfp[0]) 70 | im = Image.new("RGB", (width, height)) 71 | px = im.load() 72 | for row in range(height): 73 | for col in range(width): 74 | nfp_pixel = nfp[row][col] 75 | color_idx = int(nfp_pixel, 16) 76 | pixel_color = CC_COLORS[color_idx] 77 | px[col, row] = pixel_color 78 | return im 79 | 80 | # Colors is a list/tuple of 3-item (R, G, B) tuples. 81 | def _quantize_with_colors(image, colors, dither=0): 82 | pal_im = Image.new("P", (1, 1)) 83 | color_vals = [] 84 | for color in colors: 85 | for val in color: 86 | color_vals.append(val) 87 | color_vals = tuple(color_vals) 88 | pal_im.putpalette(color_vals + colors[-1] * (256 - len(colors))) 89 | image = image.convert(mode="RGB") 90 | return image.quantize(palette=pal_im,dither=dither) 91 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow>=6 2 | numpy>=1.16 --------------------------------------------------------------------------------