diff --git a/.gitignore b/.gitignore index 0dd8629..5f944e1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ _build .env build* bundles +.idea/ +venv/ + +.vscode diff --git a/.pylintrc b/.pylintrc index 039eaec..5e9a6dd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS +ignore=CVS,adafruit_imageload/tests # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. diff --git a/.travis.yml b/.travis.yml index 2de9871..65e9d67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ install: - pip install --force-reinstall pylint==1.9.2 script: + - pytest adafruit_imageload - pylint adafruit_imageload/** - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-imageload --library_location . diff --git a/adafruit_imageload/__init__.py b/adafruit_imageload/__init__.py index 5c36a32..3a3fcdd 100644 --- a/adafruit_imageload/__init__.py +++ b/adafruit_imageload/__init__.py @@ -43,8 +43,11 @@ def load(filename, *, bitmap=None, palette=None): """ with open(filename, "rb") as file: header = file.read(3) + file.seek(0) if header.startswith(b"BM"): from . import bmp - file.seek(0) return bmp.load(file, bitmap=bitmap, palette=palette) + if header.startswith(b"P"): + from . import pnm + return pnm.load(file, header, bitmap=bitmap, palette=palette) raise RuntimeError("Unsupported image format") diff --git a/adafruit_imageload/bmp/__init__.py b/adafruit_imageload/bmp/__init__.py index e83bd2b..26dcca3 100644 --- a/adafruit_imageload/bmp/__init__.py +++ b/adafruit_imageload/bmp/__init__.py @@ -56,9 +56,9 @@ def load(file, *, bitmap=None, palette=None): if colors == 0 and color_depth >= 16: raise NotImplementedError("True color BMP unsupported") - else: - if colors == 0: - colors = 2 ** color_depth - from . import indexed - return indexed.load(file, width, height, data_start, colors, color_depth, bitmap=bitmap, - palette=palette) + + if colors == 0: + colors = 2 ** color_depth + from . import indexed + return indexed.load(file, width, height, data_start, colors, color_depth, bitmap=bitmap, + palette=palette) diff --git a/adafruit_imageload/pnm/__init__.py b/adafruit_imageload/pnm/__init__.py new file mode 100644 index 0000000..fec8726 --- /dev/null +++ b/adafruit_imageload/pnm/__init__.py @@ -0,0 +1,103 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm` +==================================================== + +Load pixel values (indices or colors) into a bitmap and colors into a palette. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git" + + +def load(file, header, *, bitmap=None, palette=None): + """ + Scan for netpbm format info, skip over comments, and and delegate to a submodule + to do the actual data loading. + Formats P1, P4 have two space padded pieces of information: width and height. + All other formats have three: width, height, and max color value. + This load function will move the file stream pointer to the start of data in all cases. + """ + # pylint: disable=too-many-branches + magic_number = header[:2] + file.seek(2) + pnm_header = [] + next_value = bytearray() + while True: + # We have all we need at length 3 for formats P2, P3, P5, P6 + if len(pnm_header) == 3: + if magic_number in [b"P2", b"P5"]: + from . import pgm + + return pgm.load( + file, magic_number, pnm_header, bitmap=bitmap, palette=palette + ) + + if magic_number == b"P3": + from . import ppm_ascii + + return ppm_ascii.load( + file, pnm_header[0], pnm_header[1], bitmap=bitmap, palette=palette + ) + + if magic_number == b"P6": + from . import ppm_binary + + return ppm_binary.load( + file, pnm_header[0], pnm_header[1], bitmap=bitmap, palette=palette + ) + + if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]: + bitmap = bitmap(pnm_header[0], pnm_header[1], 1) + if palette: + palette = palette(1) + palette[0] = b"\xFF\xFF\xFF" + if magic_number.startswith(b"P1"): + from . import pbm_ascii + + return pbm_ascii.load( + file, pnm_header[0], pnm_header[1], bitmap=bitmap, palette=palette + ) + + from . import pbm_binary + + return pbm_binary.load( + file, pnm_header[0], pnm_header[1], bitmap=bitmap, palette=palette + ) + + next_byte = file.read(1) + if next_byte == b"": + raise RuntimeError("Unsupported image format {}".format(magic_number)) + if next_byte == b"#": # comment found, seek until a newline or EOF is found + while file.read(1) not in [b"", b"\n"]: # EOF or NL + pass + elif not next_byte.isdigit(): # boundary found in header data + if next_value: + # pull values until space is found + pnm_header.append(int("".join(["%c" % char for char in next_value]))) + next_value = bytearray() # reset the byte array + else: + next_value += next_byte # push the digit into the byte array diff --git a/adafruit_imageload/pnm/pbm_ascii.py b/adafruit_imageload/pnm/pbm_ascii.py new file mode 100644 index 0000000..2415a17 --- /dev/null +++ b/adafruit_imageload/pnm/pbm_ascii.py @@ -0,0 +1,52 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.pbm_ascii` +==================================================== + +Load pixel values (indices or colors) into a bitmap and for an ascii ppm, +return None for pallet. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git" + + +def load(file, width, height, bitmap=None, palette=None): + """ + Load a P1 'PBM' ascii image into the displayio.Bitmap + """ + next_byte = True + for y in range(height): + x = 0 + while next_byte: + next_byte = file.read(1) + if not next_byte.isdigit(): + continue + bitmap[x, y] = 1 if next_byte == b"1" else 0 + if x == width - 1: + break + x += 1 + return bitmap, palette diff --git a/adafruit_imageload/pnm/pbm_binary.py b/adafruit_imageload/pnm/pbm_binary.py new file mode 100644 index 0000000..4ebac3e --- /dev/null +++ b/adafruit_imageload/pnm/pbm_binary.py @@ -0,0 +1,74 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.pbm_binary` +==================================================== + +Load pixel values (indices or colors) into a bitmap and for an ascii ppm, +return None for pallet. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git" + + +def load(file, width, height, bitmap=None, palette=None): + """ + Load a P4 'PBM' binary image into the displayio.Bitmap + """ + x = 0 + y = 0 + while True: + next_byte = file.read(1) + if not next_byte: + break # out of bits + for bit in iterbits(next_byte): + bitmap[x, y] = bit + x += 1 + if x > width - 1: + y += 1 + x = 0 + if y > height - 1: + break + return bitmap, palette + + +def iterbits(b): + """ + generator to iterate over the bits in a byte (character) + """ + in_char = reverse(int.from_bytes(b, "little")) + for i in range(8): + yield (in_char >> i) & 1 + + +def reverse(b): + """ + reverse bit order so the iterbits works + """ + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4 + b = (b & 0xCC) >> 2 | (b & 0x33) << 2 + b = (b & 0xAA) >> 1 | (b & 0x55) << 1 + return b diff --git a/adafruit_imageload/pnm/pgm/__init__.py b/adafruit_imageload/pnm/pgm/__init__.py new file mode 100644 index 0000000..f05d536 --- /dev/null +++ b/adafruit_imageload/pnm/pgm/__init__.py @@ -0,0 +1,52 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.pgm` +==================================================== + +Load pixel values (indices or colors) into a bitmap and colors into a palette. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + + +def load(file, magic_number, header, *, bitmap=None, palette=None): + """ + Perform the load of Netpbm greyscale images (P2, P5) + """ + if header[2] > 256: + raise NotImplementedError("16 bit files are not supported") + width = header[0] + height = header[1] + + if magic_number == b"P2": # To handle ascii PGM files. + from . import ascii as pgm_ascii + + return pgm_ascii.load(file, width, height, bitmap=bitmap, palette=palette) + + if magic_number == b"P5": # To handle binary PGM files. + from . import binary + + return binary.load(file, width, height, bitmap=bitmap, palette=palette) + + raise NotImplementedError("Was not able to send image") diff --git a/adafruit_imageload/pnm/pgm/ascii.py b/adafruit_imageload/pnm/pgm/ascii.py new file mode 100644 index 0000000..38f5c22 --- /dev/null +++ b/adafruit_imageload/pnm/pgm/ascii.py @@ -0,0 +1,76 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.pgm.ascii` +==================================================== + +Load pixel values (indices or colors) into a bitmap and colors into a palette. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + + +def load(file, width, height, bitmap=None, palette=None): + """ + Load a PGM ascii file (P2) + """ + data_start = file.tell() # keep this so we can rewind + palette_colors = set() + pixel = bytearray() + # build a set of all colors present in the file, so palette and bitmap can be constructed + while True: + byte = file.read(1) + if byte == b"": + break + if not byte.isdigit(): + int_pixel = int("".join(["%c" % char for char in pixel])) + palette_colors.add(int_pixel) + pixel = bytearray() + pixel += byte + if palette: + palette = build_palette(palette, palette_colors) + if bitmap: + bitmap = bitmap(width, height, len(palette_colors)) + palette_colors = list(palette_colors) + file.seek(data_start) + for y in range(height): + for x in range(width): + pixel = bytearray() + while True: + byte = file.read(1) + if not byte.isdigit(): + break + pixel += byte + int_pixel = int("".join(["%c" % char for char in pixel])) + bitmap[x, y] = palette_colors.index(int_pixel) + return bitmap, palette + + +def build_palette(palette_class, palette_colors): + """ + construct the Palette, and populate it with the set of palette_colors + """ + palette = palette_class(len(palette_colors)) + for counter, color in enumerate(palette_colors): + palette[counter] = bytes([color, color, color]) + return palette diff --git a/adafruit_imageload/pnm/pgm/binary.py b/adafruit_imageload/pnm/pgm/binary.py new file mode 100644 index 0000000..2925280 --- /dev/null +++ b/adafruit_imageload/pnm/pgm/binary.py @@ -0,0 +1,64 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.pgm.binary` +==================================================== + +Load pixel values (indices or colors) into a bitmap and colors into a palette. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + + +def load(file, width, height, bitmap=None, palette=None): + """ + Load a P5 format file (binary), handle PGM (greyscale) + """ + palette_colors = set() + data_start = file.tell() + for y in range(height): + data_line = iter(bytes(file.read(width))) + for pixel in data_line: + palette_colors.add(pixel) + + if palette: + palette = build_palette(palette, palette_colors) + if bitmap: + bitmap = bitmap(width, height, len(palette_colors)) + palette_colors = list(palette_colors) + file.seek(data_start) + for y in range(height): + data_line = iter(bytes(file.read(width))) + for x, pixel in enumerate(data_line): + bitmap[x, y] = palette_colors.index(pixel) + return bitmap, palette + + +def build_palette(palette_class, palette_colors): + """ + construct the Palette, and populate it with the set of palette_colors + """ + palette = palette_class(len(palette_colors)) + for counter, color in enumerate(palette_colors): + palette[counter] = bytes([color, color, color]) + return palette diff --git a/adafruit_imageload/pnm/ppm_ascii.py b/adafruit_imageload/pnm/ppm_ascii.py new file mode 100644 index 0000000..013023f --- /dev/null +++ b/adafruit_imageload/pnm/ppm_ascii.py @@ -0,0 +1,89 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.ppm_ascii` +==================================================== + +Load pixel values (indices or colors) into a bitmap and for an ascii ppm, +return None for pallet. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git" + + +def load(file, width, height, bitmap=None, palette=None): + """ + :param stream file: infile with the position set at start of data + :param int width: + :param int height: + :param int max_colors: color space of file + :param bitmap: displayio.Bitmap class + :param palette: displayio.Palette class + :return tuple: + """ + palette_colors = set() + data_start = file.tell() + for triplet in read_three_colors(file): + palette_colors.add(triplet) + + if palette: + palette = palette(len(palette_colors)) + for counter, color in enumerate(palette_colors): + palette[counter] = color + if bitmap: + file.seek(data_start) + bitmap = bitmap(width, height, len(palette_colors)) + palette_colors = list(palette_colors) + for y in range(height): + for x in range(width): + for color in read_three_colors(file): + bitmap[x, y] = palette_colors.index(color) + break # exit the inner generator + return bitmap, palette + + +def read_three_colors(file): + """ + Generator to read integer values from file, in groups of three. + Each value can be len 1-3, for values 0 - 255, space padded. + :return tuple[int]: + """ + triplet = [] + color = bytearray() + while True: + this_byte = file.read(1) + if this_byte.isdigit(): + color += this_byte + # not a digit means we completed one number (found a space separator or EOF) + elif color or (triplet and this_byte == b""): + triplet.append(int("".join(["%c" % char for char in color]))) + color = bytearray() + if len(triplet) == 3: # completed one pixel + yield bytes(tuple(triplet)) + triplet = [] + # short circuit must be after all other cases, so we yield the last pixel before returning + if this_byte == b"": + return diff --git a/adafruit_imageload/pnm/ppm_binary.py b/adafruit_imageload/pnm/ppm_binary.py new file mode 100644 index 0000000..d32d71a --- /dev/null +++ b/adafruit_imageload/pnm/ppm_binary.py @@ -0,0 +1,69 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.pnm.ppm_binary` +==================================================== + +Load pixel values (indices or colors) into a bitmap and for a binary ppm, +return None for pallet. + +* Author(s): Matt Land, Brooke Storm, Sam McGahan + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git" + + +def load(file, width, height, bitmap=None, palette=None): + """Load pixel values (indices or colors) into a bitmap and for a binary + ppm, return None for pallet.""" + + data_start = file.tell() + palette_colors = set() + line_size = width * 3 + + for y in range(height): + data_line = iter(bytes(file.read(line_size))) + for red in data_line: + # red, green, blue + palette_colors.add((red, next(data_line), next(data_line))) + + if palette: + palette = palette(len(palette_colors)) + for counter, color in enumerate(palette_colors): + palette[counter] = bytes(color) + if bitmap: + bitmap = bitmap(width, height, len(palette_colors)) + file.seek(data_start) + palette_colors = list(palette_colors) + for y in range(height): + x = 0 + data_line = iter(bytes(file.read(line_size))) + for red in data_line: + # red, green, blue + bitmap[x, y] = palette_colors.index( + (red, next(data_line), next(data_line)) + ) + x += 1 + + return bitmap, palette diff --git a/adafruit_imageload/tests/__init__.py b/adafruit_imageload/tests/__init__.py new file mode 100644 index 0000000..d0cbda7 --- /dev/null +++ b/adafruit_imageload/tests/__init__.py @@ -0,0 +1,34 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_bmp_indexed_load` +==================================================== + +* Author(s): Matt Land + +This is the location for tests. +Small example image files can be placed in examples/images, +and tests written to load them go in this module. + +See also: docs/developing.rst + +""" diff --git a/adafruit_imageload/tests/displayio_shared_bindings.py b/adafruit_imageload/tests/displayio_shared_bindings.py new file mode 100644 index 0000000..bcbc49d --- /dev/null +++ b/adafruit_imageload/tests/displayio_shared_bindings.py @@ -0,0 +1,208 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.displayio_shared_bindings` +==================================================== + +The classes in this file are designed to emulate Circuitpython's displayio classes +for Bitmap and Palette. These mimic classes should have the same methods and interface as the real interface, +but with extra validation checks, warnings, and messages to facilitate debugging. + +Code that can be run successfully against these classes will have a good chance of + working correctly on hardware running Circuitpython, but without needing to upload code to a board + after each attempt. + +* Author(s): Matt Land + +""" +from typing import Union + + +class Bitmap_C_Interface(object): + """ + A class to simulate the displayio.Bitmap class for testing, based on + https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/Bitmap.html + In case of discrepancy, the C implementation takes precedence. + """ + + def __init__(self, width: int, height: int, colors: int) -> None: + self.width = width + self.height = height + self.colors = colors + self.data = {} + + def _abs_pos(self, width: int, height: int) -> int: + if height >= self.height: + raise ValueError("height > max") + if width >= self.width: + raise ValueError("width > max") + return width + (height * self.width) + + def _decode(self, position: int) -> tuple: + return position % self.width, position // self.width + + def __setitem__(self, key: Union[tuple, int], value: int) -> None: + """ + Set using x, y coordinates, or absolution position + bitmap[0] = 1 + bitmap[2,1] = 5 + """ + if isinstance(key, tuple): + # order is X, Y from the docs + # https://github.com/adafruit/circuitpython/blob/master/shared-bindings/displayio/Bitmap.c + self.__setitem__(self._abs_pos(key[0], key[1]), value) + return + if not isinstance(value, (int)): + raise RuntimeError(f"set value as int, not {type(value)}") + if value > 255: + raise ValueError(f"pixel value {value} too large") + if self.data.get(key): + raise ValueError(f"pixel {self._decode(key)}/{key} already set, cannot set again") + self.data[key] = value + + def __getitem__(self, item: Union[tuple, int]) -> bytearray: + if isinstance(item, tuple): + return self.__getitem__(self._abs_pos(item[0], item[1])) + if item > self.height * self.width: + raise RuntimeError(f"get position out of range {item}") + try: + return self.data[item] + except KeyError: + raise RuntimeError("no data at {} [{}]".format(self._decode(item), item)) + + def validate(self, detect_empty_image=True) -> None: + """ + method to to make sure all pixels allocated in the Bitmap + were set with a value + """ + seen_colors = set() + if not self.data: + raise ValueError("no rows were set / no data in memory") + for y in range(self.height): + for x in range(self.width): + try: + seen_colors.add(self[x,y]) + except KeyError: + raise ValueError(f"missing data at {x},{y}") + if detect_empty_image and len(seen_colors) < 2: + raise ValueError('image detected as only one color. set detect_empty_image=False to ignore') + + def __str__(self) -> str: + """ + method to dump the contents of the Bitmap to a terminal, + for debugging purposes + + Example: + -------- + + bitmap = Bitmap(5, 4, 4) + ... # assign bitmap values + print(str(bitmap)) + """ + out = "\n" + for y in range(self.height): + for x in range(self.width): + data = self[x, y] + out += f"{data:>4}" + out += "\n" + return out + + +class Palette_C_Interface(object): + """ + A class to simulates the displayio.Palette class for testing, based on + https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/Palette.html + In case of discrepancy, the C implementation takes precedence. + """ + + def __init__(self, num_colors: int) -> None: + self.num_colors = num_colors + self.colors = {} + + def __setitem__(self, key: int, value: Union[bytes, int, bytearray]) -> None: + """ + Set using zero indexed color value + palette = Palette(1) + palette[0] = 0xFFFFFF + + """ + if key >= self.num_colors: + raise ValueError( + f"palette index {key} is greater than allowed by num_colors {self.num_colors}" + ) + if not isinstance(value, (bytes, int, bytearray)): + raise ValueError(f"palette color should be bytes, not {type(value)}") + if isinstance(value, int) and value > 0xFFFFFF: + raise ValueError(f"palette color int {value} is too large") + if self.colors.get(key): + raise ValueError( + f"palette color {key} was already set, should not reassign" + ) + self.colors[key] = value + + def __getitem__(self, item: int) -> Union[bytes, int, bytearray]: + """ + Warning: this method is not supported in the actual C interface. + It is provided here for debugging purposes. + """ + if item >= self.num_colors: + raise ValueError( + f"palette index {item} should be less than {self.num_colors}" + ) + if not self.colors.get(item): + raise ValueError(f"palette index {item} is not set") + return self.colors[item] + + def validate(self): + """ + method to make sure all colors allocated in Palette were set to a value + """ + if not self.colors: + raise IndexError("no palette colors were set") + if len(self.colors) != self.num_colors: + raise IndexError( + "palette was initialized for {} colors, but only {} were inserted".format( + self.num_colors, len(self.colors) + ) + ) + for i in range(self.num_colors): + try: + self.colors + except IndexError: + raise ValueError("missing color `{}` in palette color list".format(i)) + + def __str__(self): + """ + method to dump the contents of the Palette to a terminal, + for debugging purposes + + Example: + -------- + + palette = Palette(1) + palette[0] = 0xFFFFFF + print(str(palette)) + """ + out = "\nPalette:\n" + for y in range(len(self.colors)): + out += f" [{y}] {self.colors[y]}\n" + return out diff --git a/adafruit_imageload/tests/test_bitmap_c_interface.py b/adafruit_imageload/tests/test_bitmap_c_interface.py new file mode 100644 index 0000000..4055f51 --- /dev/null +++ b/adafruit_imageload/tests/test_bitmap_c_interface.py @@ -0,0 +1,108 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_bitmap_c_interface` +==================================================== + +These tests are to validate the displayio_shared_bindings classes that other tests are built on. + +* Author(s): Matt Land + +""" +from unittest import TestCase +from .displayio_shared_bindings import Bitmap_C_Interface + + +class TestBitmap_C_Interface(TestCase): + def test_init(self): + b = Bitmap_C_Interface(2, 4, 1) + self.assertEqual(2, b.width) + self.assertEqual(4, b.height) + self.assertEqual(1, b.colors) + + def test_abs(self): + b = Bitmap_C_Interface(5, 2, 1) + self.assertEqual(9, b._abs_pos(4, 1)) + + def test_set_tuple(self): + b = Bitmap_C_Interface(2, 4, 1) + b[1, 3] = 67 + self.assertEqual(b[1, 3], 67) + + def test_set_abs(self): + b = Bitmap_C_Interface(2, 4, 1) + b[0] = 42 + self.assertEqual(b[0], 42) + + def test_abs_and_tuple(self): + b = Bitmap_C_Interface(2, 4, 1) + b[7] = 101 + self.assertEqual(101, b[1, 3]) + + def test_non_zero(self): + b = Bitmap_C_Interface(2, 4, 1) + b[1, 1] = 100 + self.assertEqual(100, b[1, 1]) + + def test_throws_x_out_of_range(self): + b = Bitmap_C_Interface(2, 4, 1) + try: + b[2, 1] = 100 + self.fail("should have thrown") + except ValueError: + pass + + def test_max(self): + b = Bitmap_C_Interface(2, 4, 1) + b[1, 1] = 66 + self.assertEqual(66, b[1, 1]) + + def test_uninitialized(self): + b = Bitmap_C_Interface(2, 4, 1) + try: + b[1, 1] + self.fail("should have thrown") + except RuntimeError: + pass + + def test_validate_throws(self): + b = Bitmap_C_Interface(2, 4, 1) + try: + b.validate() + except ValueError: + pass + + def test_repr(self): + b = Bitmap_C_Interface(3, 2, 1) + b[0, 0] = 1 + b[1, 0] = 0 + b[2, 0] = 0 + b[0, 1] = 1 + b[1, 1] = 1 + b[2, 1] = 0 + self.assertEqual("\n 1 0 0\n 1 1 0\n", str(b)) + + def test_decode(self): + b = Bitmap_C_Interface(4, 4, 1) + self.assertEqual((0, 0), b._decode(0)) + encoded = b._abs_pos(3, 3) + self.assertEqual((3, 3), b._decode(encoded)) diff --git a/adafruit_imageload/tests/test_bmp_indexed_load.py b/adafruit_imageload/tests/test_bmp_indexed_load.py new file mode 100644 index 0000000..53d1544 --- /dev/null +++ b/adafruit_imageload/tests/test_bmp_indexed_load.py @@ -0,0 +1,61 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_bmp_indexed_load` +==================================================== + +* Author(s): Matt Land + +""" + +import os +from unittest import TestCase +from .. import load +from .displayio_shared_bindings import Bitmap_C_Interface, Palette_C_Interface + + +class TestBmpIndexedLoad(TestCase): + def test_order_bgra_to_rgba(self): + test_file = os.path.join( + os.path.dirname(__file__), "..", "..", "examples", "images", "4bit.bmp" + ) + + bitmap, palette = load( + filename=test_file, bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface), bitmap) + self.assertEqual(16, bitmap.colors) + self.assertEqual(15, bitmap.width) + self.assertEqual(17, bitmap.height) + + bitmap.validate() + # uncomment line below to see a string representation of the object + # self.fail(str(bitmap)) + self.assertEqual(5, bitmap[0]) # check first row + self.assertEqual(5, bitmap[11, 1]) # check second row + + self.assertEqual(16, palette.num_colors) + palette.validate() + # make sure bye order swapped + self.assertTrue(palette[4] in [b"\x9d\x00\xff\x00", b"\x9d\x00\xff"]) + # uncomment line below to see a string representation of the object + # self.fail(str(palette)) diff --git a/adafruit_imageload/tests/test_palette_c_interface.py b/adafruit_imageload/tests/test_palette_c_interface.py new file mode 100644 index 0000000..909a772 --- /dev/null +++ b/adafruit_imageload/tests/test_palette_c_interface.py @@ -0,0 +1,100 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_palette_c_interface` +==================================================== + +These tests are to validate the displayio_shared_bindings classes that other tests are built on. + +* Author(s): Matt Land + +""" +from unittest import TestCase +from .displayio_shared_bindings import Palette_C_Interface + + +class TestPalette_C_Interface(TestCase): + def test_init_mono(self): + Palette_C_Interface(1) + + def test_init_color(self): + Palette_C_Interface(256) + + def test_set_int(self): + palette = Palette_C_Interface(1) + palette[0] = 0xFFFFFF + + def test_get_int(self): + palette = Palette_C_Interface(1) + palette[0] = 0xFFFFFF + self.assertEqual(0xFFFFFF, palette[0]) + + def test_set_byte(self): + palette = Palette_C_Interface(1) + palette[0] = b"\xFF\xFF\xFF" + + def test_get_byte(self): + palette = Palette_C_Interface(1) + palette[0] = b"\xFF\xFF\xFF" + self.assertEqual(b"\xFF\xFF\xFF", palette[0]) + + def test_set_bytearray(self): + palette = Palette_C_Interface(1) + palette[0] = bytearray(b"\xFF\xFF\xFF") + + def test_prevents_out_of_range(self): + palette = Palette_C_Interface(1) + try: + palette[1] = 0xFFFFFF + self.fail("exception should have already thrown") + except ValueError as e: + if "greater than allowed" not in str(e): + raise + + def test_prevents_set_non_allowed(self): + palette = Palette_C_Interface(1) + try: + palette[0] = "\xFF\xFF\xFF" # attempt with a string, which is not allowed + self.fail("exception should have thrown") + except ValueError as e: + if "should be" not in str(e): + raise + + def test_validate_success(self): + palette = Palette_C_Interface(1) + palette[0] = b"\xFF\xFF\xFF" + palette.validate() + + def test_validate_fails(self): + palette = Palette_C_Interface(2) + palette[1] = b"\xFF\xFF\xFF" + try: + palette.validate() + self.fail("exception should have thrown") + except IndexError as e: + if "palette was initialized" not in str(e): + raise + + def test_str(self): + palette = Palette_C_Interface(1) + palette[0] = b"\xFF\xFF\xFF" + print(str(palette)) diff --git a/adafruit_imageload/tests/test_pbm_load.py b/adafruit_imageload/tests/test_pbm_load.py new file mode 100644 index 0000000..4dd9edf --- /dev/null +++ b/adafruit_imageload/tests/test_pbm_load.py @@ -0,0 +1,141 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_pbm_load` +==================================================== + +* Author(s): Matt Land + +""" +import os +from io import BytesIO +from unittest import TestCase +from .. import pnm +from ..pnm.pbm_binary import iterbits, reverse +from .displayio_shared_bindings import Bitmap_C_Interface, Palette_C_Interface + + +class TestPbmLoad(TestCase): + def test_load_fails_with_no_header_data(self): + file = BytesIO(b"some initial binary data: \x00\x01") + try: + pnm.load( + file, b"P1", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.fail("should have failed") + except Exception as caught_exception: + if "Unsupported image format" not in str(caught_exception): + raise + + def test_load_works_p1_ascii(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p1_mono_ascii.pbm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P1", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface), bitmap) + self.assertEqual(1, bitmap.colors) + self.assertEqual(13, bitmap.width) + self.assertEqual(21, bitmap.height) + + bitmap.validate() + self.assertEqual(0, bitmap[0]) # check first row + self.assertEqual(1, bitmap[11, 1]) # check second row + + self.assertEqual(1, palette.num_colors) + palette.validate() + + def test_load_works_p4_in_mem(self): + file = BytesIO(b"P4\n4 2\n\x55") + bitmap, palette = pnm.load( + file, b"P4", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertEqual(4, bitmap.width) + self.assertEqual(2, bitmap.height) + bitmap.validate() + self.assertEqual("\n 0 1 0 1\n 0 1 0 1\n", str(bitmap)) + palette.validate() + + def test_load_works_p4_binary(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p4_mono_binary.pbm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P4", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertEqual(1, palette.num_colors) + palette.validate() + self.assertEqual(b"\xff\xff\xff", palette[0]) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface)) + self.assertEqual(1, bitmap.colors) + self.assertEqual(32, bitmap.width) + self.assertEqual(15, bitmap.height) + bitmap.validate() + + def test_load_works_p4_binary_high_res(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p4_mono_large.pbm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P4", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface)) + self.assertEqual(1, bitmap.colors) + self.assertEqual(320, bitmap.width) + self.assertEqual(240, bitmap.height) + bitmap.validate() + self.assertEqual(1, palette.num_colors) + palette.validate() + + def test_iterbits(self): + k = b'k' + bits = [] + for byte in iterbits(k): + bits.append(byte) + #self.assertEqual([0,1,1,0,1,0,1,1], bits[::-1]) + self.assertEqual([0,1,1,0,1,0,1,1], bits) + + def test_reverse(self): + # 00110100 to 00101100 + self.assertEqual(reverse(0x34), 0x2C) + self.assertEqual(reverse(0xFF), 0xFF) + self.assertEqual(reverse(0x00), 0x00) + self.assertEqual(reverse(0x0E), 0x70) \ No newline at end of file diff --git a/adafruit_imageload/tests/test_pgm_load.py b/adafruit_imageload/tests/test_pgm_load.py new file mode 100644 index 0000000..a7021fa --- /dev/null +++ b/adafruit_imageload/tests/test_pgm_load.py @@ -0,0 +1,79 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_bgm_load` +==================================================== + +* Author(s): Matt Land + +""" +import os +from unittest import TestCase +from .. import pnm +from .displayio_shared_bindings import Bitmap_C_Interface, Palette_C_Interface + + +class TestPgmLoad(TestCase): + def test_load_works_p2_ascii(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p2_ascii.pgm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P2", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface), bitmap) + self.assertEqual(6, bitmap.colors) + self.assertEqual(8, bitmap.width) + self.assertEqual(8, bitmap.height) + bitmap.validate() + self.assertEqual(6, palette.num_colors) + palette.validate() + # self.fail(str(palette)) + + def test_load_works_p5_binary(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p5_binary.pgm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P5", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface), bitmap) + + self.assertEqual(8, palette.num_colors) + palette.validate() + self.assertEqual(8, bitmap.colors) + self.assertEqual(8, bitmap.width) + self.assertEqual(8, bitmap.height) + bitmap.validate() + # self.fail(str(bitmap)) diff --git a/adafruit_imageload/tests/test_ppm_load.py b/adafruit_imageload/tests/test_ppm_load.py new file mode 100644 index 0000000..b0cea52 --- /dev/null +++ b/adafruit_imageload/tests/test_ppm_load.py @@ -0,0 +1,92 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Matt Land +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.tests.test_ppm_load` +==================================================== + +* Author(s): Matt Land + +""" +import os +from io import BytesIO +from unittest import TestCase +from .. import pnm +from ..pnm.ppm_ascii import read_three_colors +from .displayio_shared_bindings import Bitmap_C_Interface, Palette_C_Interface + + +class TestPpmLoad(TestCase): + def test_load_works_p3_ascii(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p3_rgb_ascii.ppm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P3", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) # type: Bitmap_C_Interface, Palette_C_Interface + + self.assertTrue(isinstance(palette, Palette_C_Interface)) + self.assertEqual(6, palette.num_colors) + palette.validate() + #self.fail(str(palette)) + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface), bitmap) + self.assertEqual(6, bitmap.colors) + self.assertEqual(16, bitmap.width) + self.assertEqual(16, bitmap.height) + bitmap.validate() + + def test_load_works_p6_binary(self): + test_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "examples", + "images", + "netpbm_p6_binary.ppm", + ) + with open(test_file, "rb") as file: + bitmap, palette = pnm.load( + file, b"P6", bitmap=Bitmap_C_Interface, palette=Palette_C_Interface + ) # type: Bitmap_C_Interface, Palette_C_Interface + self.assertEqual(6, palette.num_colors) + palette.validate() + self.assertTrue(isinstance(bitmap, Bitmap_C_Interface), bitmap) + self.assertEqual(6, bitmap.colors) + self.assertEqual(16, bitmap.width) + self.assertEqual(16, bitmap.height) + bitmap.validate() + + def test_load_three_colors_tail(self): + buffer = BytesIO(b'211 222 233') + for i in read_three_colors(buffer): + self.assertEqual(b'\xd3\xde\xe9', i) + + def test_load_three_colors_middle(self): + buffer = BytesIO(b'0 128 255 45 55 25') + for i in iter(read_three_colors(buffer)): + self.assertEqual(b'\x00\x80\xff', i) + break diff --git a/examples/imageload_netpbm.py b/examples/imageload_netpbm.py new file mode 100644 index 0000000..a29396c --- /dev/null +++ b/examples/imageload_netpbm.py @@ -0,0 +1,45 @@ +""" +This is a hardware testing script for PBM +Tested with Feather M4 Express and 2.4" Featherwing +1. Flash board to 4x +2. add 'adafruit_ili9341.mpy' for 4x firmware to /lib/ on board +3. add /examples/images to /images on board +4. copy ./adafruit_imageload to /lib/ on the board +5. paste this file into code.py + +""" + +import board +import displayio +import adafruit_ili9341 +import adafruit_imageload + +spi = board.SPI() +tft_cs = board.D9 +tft_dc = board.D10 + +displayio.release_displays() +display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs) + +display = adafruit_ili9341.ILI9341(display_bus, width=320, height=240) + +# Make the display context +splash = displayio.Group(max_size=10) +display.show(splash) +#image = "images/netpbm_p1_mono_ascii.pbm" +#image = "images/netpbm_p2_ascii.pgm" +#image = "images/netpbm_p3_rgb_ascii.ppm" +#image = "images/netpbm_p4_mono_binary.pbm" +#image = "images/netpbm_p5_binary.pgm" +image = "images/netpbm_p6_binary.ppm" + +bitmap, palette = adafruit_imageload.load( + image, bitmap=displayio.Bitmap, palette=displayio.Palette +) + + +bg_sprite = displayio.TileGrid(bitmap, pixel_shader=palette, x=0, y=0) +splash.append(bg_sprite) + +while True: + pass diff --git a/examples/images/netpbm_p1_mono_ascii.pbm b/examples/images/netpbm_p1_mono_ascii.pbm new file mode 100644 index 0000000..af607af --- /dev/null +++ b/examples/images/netpbm_p1_mono_ascii.pbm @@ -0,0 +1,24 @@ +P1 +# This is an example bitmap of the letter "J" in a 2x2 grid +13 21 +0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 1 0 0 0 1 0 1 0 0 0 1 0 +0 0 1 1 1 0 0 0 1 1 1 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 0 0 0 0 1 0 0 0 0 0 1 0 +0 1 0 0 0 1 0 1 0 0 0 1 0 +0 0 1 1 1 0 0 0 1 1 1 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/examples/images/netpbm_p2_ascii.pgm b/examples/images/netpbm_p2_ascii.pgm new file mode 100644 index 0000000..c807b1a --- /dev/null +++ b/examples/images/netpbm_p2_ascii.pgm @@ -0,0 +1,10 @@ +P2 +8 8 255 +0 255 0 203 128 0 128 0 +128 0 84 0 0 84 0 255 +0 84 0 255 62 0 84 0 +128 0 62 0 0 255 0 203 +203 0 255 0 0 62 0 128 +0 84 0 62 255 0 84 0 +255 0 84 0 0 84 0 128 +0 128 0 128 203 0 255 0 \ No newline at end of file diff --git a/examples/images/netpbm_p3_rgb_ascii.ppm b/examples/images/netpbm_p3_rgb_ascii.ppm new file mode 100644 index 0000000..5030536 --- /dev/null +++ b/examples/images/netpbm_p3_rgb_ascii.ppm @@ -0,0 +1,3 @@ +P3 +16 16 255 +255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 109 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 109 0 255 109 0 255 168 121 251 109 0 255 109 0 255 109 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 109 0 255 168 121 251 168 121 251 168 121 251 168 121 251 168 121 251 109 0 255 109 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 109 0 255 109 0 255 168 121 251 214 0 255 214 0 255 214 0 255 168 121 251 168 121 251 109 0 255 109 0 255 109 0 255 109 0 255 109 0 255 255 255 255 255 255 255 255 255 255 109 0 255 168 121 251 168 121 251 214 0 255 214 0 255 214 0 255 214 0 255 168 121 251 168 121 251 168 121 251 168 121 251 168 121 251 109 0 255 109 0 255 255 255 255 255 255 255 109 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 214 0 255 109 0 255 109 0 255 255 255 255 255 255 255 109 0 255 230 121 251 230 121 251 230 121 251 230 121 251 230 121 251 214 0 255 230 121 251 230 121 251 230 121 251 230 121 251 230 121 251 109 0 255 255 255 255 255 255 255 255 255 255 255 255 255 109 0 255 109 0 255 109 0 255 109 0 255 230 121 251 230 121 251 230 121 251 109 0 255 109 0 255 109 0 255 109 0 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 109 0 255 109 0 255 109 0 255 109 0 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 121 149 251 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 121 149 251 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 \ No newline at end of file diff --git a/examples/images/netpbm_p4_mono_binary.pbm b/examples/images/netpbm_p4_mono_binary.pbm new file mode 100644 index 0000000..e6fd761 Binary files /dev/null and b/examples/images/netpbm_p4_mono_binary.pbm differ diff --git a/examples/images/netpbm_p4_mono_large.pbm b/examples/images/netpbm_p4_mono_large.pbm new file mode 100644 index 0000000..72c0431 Binary files /dev/null and b/examples/images/netpbm_p4_mono_large.pbm differ diff --git a/examples/images/netpbm_p5_binary.pgm b/examples/images/netpbm_p5_binary.pgm new file mode 100644 index 0000000..cfb2888 Binary files /dev/null and b/examples/images/netpbm_p5_binary.pgm differ diff --git a/examples/images/netpbm_p6_binary.ppm b/examples/images/netpbm_p6_binary.ppm new file mode 100644 index 0000000..2c3a32f Binary files /dev/null and b/examples/images/netpbm_p6_binary.ppm differ