diff --git a/__pycache__/imageDataTools.cpython-38.pyc b/__pycache__/imageDataTools.cpython-38.pyc new file mode 100644 index 0000000..859e7d5 Binary files /dev/null and b/__pycache__/imageDataTools.cpython-38.pyc differ diff --git a/__pycache__/imageTools.cpython-38.pyc b/__pycache__/imageTools.cpython-38.pyc new file mode 100644 index 0000000..33a955d Binary files /dev/null and b/__pycache__/imageTools.cpython-38.pyc differ diff --git a/imageDataTools.py b/imageDataTools.py new file mode 100644 index 0000000..001a966 --- /dev/null +++ b/imageDataTools.py @@ -0,0 +1,165 @@ +""" + +python2 or python3. + +""" + +import itertools +from collections import deque + + + +ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +#case is ignored by builtin int(), but int() can't go above base 36 anyway, so it probably doesn't matter. + +def assert_equals(thing0, thing1): + assert thing0 == thing1, "{} does not equal {}.".format(thing0, thing1) + + + +def gen_chunks(input_seq, length): + if length < 1: + raise ValueError("chunk length must be at least 1.") + labeling_function = (lambda toLabel: toLabel[0]//length) + labled_group_gen = itertools.groupby(enumerate(input_seq), labeling_function) + unlabled_group_gen = (labeledGroup[1] for labeledGroup in labled_group_gen) + #remove enumeration: + result = ((itemB[1] for itemB in unlabledGroup) for unlabledGroup in unlabled_group_gen) + return result + + + +def int_to_base(value, base): + if not value >= 0: + raise ValueError("negative value: {}. Value must be positive.".format(value)) + if not base >= 2: + raise ValueError("invalid base: {}.".format(base)) + result = deque([]) + while value > 0: + current_digit = value % base + result.appendleft(current_digit) + value //= base + if len(result) == 0: + result.appendleft(0) + assert len(result) >= 1 + return list(result) + + +def int_seq_to_str(int_seq): + return "".join(ALPHABET[item] for item in int_seq) + + +def int_to_base_str(value, base): + return int_seq_to_str(int_to_base(value, base)) + + + +def int8rgb_color_is_valid(color): + return all(( + len(color) == 3, + min(color) >= 0, + max(color) < 256, + all(isinstance(componentValue, int) for componentValue in color) + )) + + +def int8rgb_color_component_to_hex(component, length=2): + + if not 0 < length <= 2: + raise ValueError("invalid length {}.".format(length)) + if not 0 <= component < 256: + raise ValueError("invalid component {}.".format(component)) + + result = int_to_base_str(component, 16) + result = result.rjust(2, ALPHABET[0]) + assert len(result) == 2 + result = result[:length] + return result + + +def int8rgb_color_to_hex( + color, length=2, + component_header="", component_delimiter="", + ): + + if not 0 < length <= 2: + raise ValueError("invalid length {}.".format(length)) + if not int8rgb_color_is_valid(color): + raise ValueError("invalid color {}.".format(color)) + + component_text_gen = (component_header + int8rgb_color_component_to_hex(value, length=length) for value in color) + + return component_delimiter.join(component_text_gen) + + +def int8rgb_pixels_to_hex( + pixels, chars_per_channel=2, + pixel_header="", pixel_delimiter="", + component_header="", component_delimiter="", + ): + + pixel_text_gen = (pixel_header + int8rgb_color_to_hex( + color, + length=chars_per_channel, + component_header=component_header, component_delimiter=component_delimiter, + ) for color in pixels) + + return pixel_delimiter.join(pixel_text_gen) + + +def int8rgb_pixel_rows_to_hex( + pixels, + auto_row_length=None, + row_header="", row_delimiter="", + **other_format_kwargs, + ): + + if auto_row_length is not None: + input_row_generator = gen_chunks(pixels, length=auto_row_length) + else: + input_row_generator = pixels + output_row_generator = (row_header + int8rgb_pixels_to_hex(pixel_row, **other_format_kwargs) for pixel_row in input_row_generator) + return row_delimiter.join(output_row_generator) + + + +def gen_triplet_tuples_from_int_seq(int_seq): + current_triplet = [] + for i, item in enumerate(int_seq): + current_triplet.append(item) + if len(current_triplet) == 3: + yield tuple(current_triplet) + current_triplet = [] + if not len(current_triplet) == 0: + raise ValueError("int seq had invalid length {} (not divisible by 3).".format(i)) + + +def int_lists_to_triplet_tuple_lists(int_list_seq): + result = [] + for int_list in int_list_seq: + tripletTupleList = list(gen_triplet_tuples_from_int_seq(int_list)) + result.append(tripletTupleList) + return result + + + + + +def test(): + assert_equals(int8rgb_color_to_hex([255,255,255], component_delimiter="!"), "ff!ff!ff") + + for test_int in [0,1,2,3,5,8,13,21,34,55,127,128,129,255]: + assert_equals(int(int8rgb_color_component_to_hex(test_int), 16), test_int) + + assert_equals(int8rgb_color_to_hex([14,15,16], length=2, component_header="?", component_delimiter="!"), "?0e!?0f!?10") + assert_equals(int8rgb_color_to_hex([14,15,16], length=1, component_header="?", component_delimiter="!"), "?0!?0!?1") + + assert_equals(int8rgb_pixels_to_hex([[14,15,16], [30,31,32]], chars_per_channel=2, pixel_header="#", component_delimiter=","), "#0e,0f,10#1e,1f,20") + + assert_equals(int8rgb_pixel_rows_to_hex([[14,15,16], [30,31,32]], auto_row_length=2), int8rgb_pixels_to_hex([[14,15,16], [30,31,32]])) + + assert_equals(int_lists_to_triplet_tuple_lists([[1,2,3,4,5,6],[7,8,9,10,11,12]]), [[(1,2,3),(4,5,6)],[(7,8,9),(10,11,12)]]) + +test() + + diff --git a/imageTools.py b/imageTools.py new file mode 100644 index 0000000..2abfd62 --- /dev/null +++ b/imageTools.py @@ -0,0 +1,203 @@ +""" + +Python3 recommended. + +""" + +import os +import fnmatch +import warnings + +from PIL import Image + +import imageDataTools +from imageDataTools import assert_equals + + + +def get_image_int8rgb_pixels(filename): + try: + image = Image.open(filename) #image is now of type PngImageFile, not Image. + imageData = image.getdata() #imageData is now of type ImagingCore, not list. + ARGBPixels = [item for item in imageData] #these may be rgb already, depending on the input image. + try: + RGBPixels = [item[:3] for item in ARGBPixels] + except TypeError: + #this is because Pillow returns data in different formats depending on which file it is reading. + raise IOError("File could not be properly read by Pillow.") + finally: + image.close() + return RGBPixels + + +def save_int8rgb_tuples_as_half_hex( + data, + output_filename=None, mode=None, + decoration="{output_filename} = \"{result}\"", + **format_kwargs, + ): + #does not save when output_filename is None. + warnings.warn("save_int8rgb_tuples_as_half_hex is deprecated. Use imageDataTools.int8rgb_pixels_to_hex and/or save_text_to_file instead.") + + if decoration is None: + decoration = "{result}" + if output_filename is not None: + if mode is None: + raise ValueError("mode not specified") + + result = imageDataTools.int8rgb_pixels_to_hex(data, chars_per_channel=1, **format_kwargs) + if output_filename is None: + return result + else: + with open(output_filename, mode) as output_file: + #assert "{output_filename}" in decoration + assert "{result}" in decoration + decorated_result = decoration.replace("{output_filename}", output_filename).replace("{result}", result) + output_file.write(decorated_result) + return result + + +def save_text_to_file(text, output_filename, mode): + + with open(output_filename, mode) as output_file: + output_file.write(text) + + +def gen_matching_file_names(folder_name, file_name_pattern): + + for testFileName in os.listdir(folder_name): + if fnmatch.fnmatch(testFileName, file_name_pattern): + yield testFileName + + +def gen_matching_png_file_names_and_pixels( + folder_name, file_name_pattern, + skip_errors=True, supress_warnings=False, + ): + + for scanFileName in gen_matching_file_names(folder_name, file_name_pattern): + scanFullFileName = folder_name + "/" + scanFileName + try: + scanFilePixels = get_image_int8rgb_pixels(scanFullFileName) + except IOError as ioe: + if skip_errors: + if not supress_warnings: + print("Skipping file {} because it couldn't be read using the current Pillow settings.".format(scanFullFileName)) + else: + raise ioe + continue + yield scanFileName, scanFilePixels + + +def process_png_files( + folder_name, + file_name_pattern="*.png", + do_console_output=True, do_file_output=False, + common_output_filename=None, mode="w", + output_filename_prefix="outputs/", output_filename_suffix=".txt", + console_line_length=48, file_line_length=None, + console_output_delimiter="\n---\n", console_output_decoration="{name}:\n{content}", + file_output_delimiter="", file_output_decoration="\n{name} = \"{content}\"\n", + skip_errors=True, supress_warnings=False, + ): + """ + folder_name - folder to search for input files, without trailing slash. + file_name_pattern - bash-like, e.g. "*.png" or "*x*.png". + do_console_output - controls whether all processed files will be printed. + do_file_output - controls whether any files will be created or modified. + common_output_filename - when not None, this option forces all file\ + outputs to be added to the same output file in append mode. + output_filename_prefix, output_filename_suffix - prefix and suffix to be\ + added to the base name, which is either the name of the input image\ + file or the provided common output filename. + console_line_length - custom line wrapping length for console output, or\ + None for unmodified output. + file_line_length - custom line wrapping length for writing to files, or\ + None for unmodified output. + """ + + useSingleOutputFile = (common_output_filename is not None) + + fileNamesAndPixelsGen = gen_matching_png_file_names_and_pixels( + folder_name, file_name_pattern, + skip_errors=skip_errors, supress_warnings=supress_warnings + ) + + for scanFileIndex, scanFileNameAndPixels in enumerate(fileNamesAndPixelsGen): + scanFileName, scanFilePixels = scanFileNameAndPixels + scanFilePixelsAsHalfHex = imageDataTools.int8rgb_pixels_to_hex(scanFilePixels, chars_per_channel=1) + + if do_file_output: + outputFileNameBase = common_output_filename if useSingleOutputFile else scanFileName + outputFileName = output_filename_prefix + outputFileNameBase + output_filename_suffix + + stringToSave = file_output_decoration.replace("{name}", scanFileName).replace("{content}", scanFilePixelsAsHalfHex) + + if scanFileIndex != 0 and useSingleOutputFile: + stringToSave = file_output_delimiter + stringToSave + if file_line_length is not None: + stringToSave = wrap_text(stringToSave, line_length=file_line_length) + + modeForCurrentWrite = ("a" if (useSingleOutputFile and scanFileIndex > 0) else mode) + save_text_to_file(stringToSave, output_filename=outputFileName, mode=modeForCurrentWrite) + + if do_console_output: + stringToPrint = console_output_decoration.replace("{name}", scanFileName).replace("{content}", scanFilePixelsAsHalfHex) + + if scanFileIndex != 0: + stringToPrint = console_output_delimiter + stringToPrint + if console_line_length is not None: + stringToPrint = wrap_text(stringToPrint, line_length=console_line_length) + + print(stringToPrint) + + +def gen_short_lines(text, line_length): + if line_length < 1: + raise ValueError("line length must be at least 1.") + for line in text.split("\n"): + i = 0 + while i < len(line): + yield line[i:i+line_length] + i += line_length + + +def wrap_text(text, line_length=80): + return "\n".join(gen_short_lines(text, line_length)) + + +def print_wrapped(text, line_length=80): + for line in gen_short_lines(text, line_length): + print(line) + + + +SAMPLE_IMAGE_HALF_HEX="\ +390390390390390390390000000390390390390390390390\ +390390390390390390000ff0ff0000390390390390390390\ +390390390390390390000ff0ff0000390390390390390390\ +390390390390390000ff0ff0ff0ff0000390390390390390\ +000000000000000000ff0ff0ff0ff0000000000000000000\ +000ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0000\ +390000ff0ff0ff0ff0000ff0ff0000ff0ff0ff0ff0000390\ +390390000ff0ff0ff0000ff0ff0000ff0ff0ff0000390390\ +390390390000ff0ff0000ff0ff0000ff0ff0000390390390\ +390390390000ff0ff0ff0ff0ff0ff0ff0ff0000390390390\ +390390000ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0000390390\ +390390000ff0ff0ff0ff0ff0ff0ff0ff0ff0ff0000390390\ +390000ff0ff0ff0ff0ff0000000ff0ff0ff0ff0ff0000390\ +390000ff0ff0ff0000000390390000000ff0ff0ff0000390\ +000ff0ff0000000390390390390390390000000ff0ff0000\ +000000000390390390390390390390390390390000000000" + +def test(): + assert_equals(save_int8rgb_tuples_as_half_hex(get_image_int8rgb_pixels("images/SAMPLE.png"), pixel_header=""), SAMPLE_IMAGE_HALF_HEX) + assert_equals(wrap_text("abcdefg\n1234567", line_length=3), "abc\ndef\ng\n123\n456\n7") + + +test() + + + + + diff --git a/images/SAMPLE.png b/images/SAMPLE.png new file mode 100644 index 0000000..26eb3c5 Binary files /dev/null and b/images/SAMPLE.png differ