From c5d1e3b52b770fe4dd38a1c8ad3af49d6facf1bb Mon Sep 17 00:00:00 2001 From: Igor Santarek Date: Sat, 22 May 2021 00:55:06 +0200 Subject: [PATCH] Fix for #341. :) (#488) --- addons/gdgifexporter/exporter.gd | 292 +++++++++++++ addons/gdgifexporter/gifexporter.gd | 392 ------------------ .../gdgifexporter/quantization/median_cut.gd | 247 +++++------ src/Autoload/Export.gd | 8 +- 4 files changed, 423 insertions(+), 516 deletions(-) create mode 100644 addons/gdgifexporter/exporter.gd delete mode 100644 addons/gdgifexporter/gifexporter.gd diff --git a/addons/gdgifexporter/exporter.gd b/addons/gdgifexporter/exporter.gd new file mode 100644 index 000000000..a96cff6e9 --- /dev/null +++ b/addons/gdgifexporter/exporter.gd @@ -0,0 +1,292 @@ +extends Reference + + +enum Error { + OK = 0, + EMPTY_IMAGE = 1, + BAD_IMAGE_FORMAT = 2 +} + + +var little_endian = preload('./little_endian.gd').new() +var lzw = preload('./gif-lzw/lzw.gd').new() +var converter = preload("./converter.gd") + +var last_color_table := [] +var last_transparency_index := -1 + +# File data and Header +var data := PoolByteArray([]) + + +func _init(_width: int, _height: int): + add_header() + add_logical_screen_descriptor(_width, _height) + add_application_ext("NETSCAPE", "2.0", [1, 0, 0]) + +func export_file_data() -> PoolByteArray: + return data + PoolByteArray([0x3b]) + +func add_header() -> void: + data += 'GIF'.to_ascii() + '89a'.to_ascii() + +func add_logical_screen_descriptor(width: int, height: int) -> void: + # not Global Color Table Flag + # Color Resolution = 8 bits + # Sort Flag = 0, not sorted. + # Size of Global Color Table set to 0 + # because we'll use only Local Tables + var packed_fields: int = 0b01110000 + var background_color_index: int = 0 + var pixel_aspect_ratio: int = 0 + + data += little_endian.int_to_2bytes(width) + data += little_endian.int_to_2bytes(height) + data.append(packed_fields) + data.append(background_color_index) + data.append(pixel_aspect_ratio) + +func add_application_ext(app_iden: String, app_auth_code: String, _data: Array) -> void: + var extension_introducer := 0x21 + var extension_label := 0xff + + var block_size := 11 + + data.append(extension_introducer) + data.append(extension_label) + data.append(block_size) + data += app_iden.to_ascii() + data += app_auth_code.to_ascii() + data.append(_data.size()) + data += PoolByteArray(_data) + data.append(0) + +# finds the image color table. Stops if the size gets larger than 256. +func find_color_table(image: Image) -> Dictionary: + image.lock() + var result: Dictionary = {} + var image_data: PoolByteArray = image.get_data() + + for i in range(0, image_data.size(), 4): + var color: Array = [ + int(image_data[i]), + int(image_data[i + 1]), + int(image_data[i + 2]), + int(image_data[i + 3])] + if not color in result: + result[color] = result.size() + if result.size() > 256: + break + + image.unlock() + return result + +func find_transparency_color_index(color_table: Dictionary) -> int: + for color in color_table: + if color[3] == 0: + return color_table[color] + return -1 + +func change_colors_to_codes(image: Image, + color_palette: Dictionary, + transparency_color_index: int) -> PoolByteArray: + image.lock() + var image_data: PoolByteArray = image.get_data() + var result: PoolByteArray = PoolByteArray([]) + + for i in range(0, image_data.size(), 4): + var color: Array = [ + image_data[i], + image_data[i + 1], + image_data[i + 2], + image_data[i + 3]] + + if color in color_palette: + if color[3] == 0 and transparency_color_index != -1: + result.append(transparency_color_index) + else: + result.append(color_palette[color]) + else: + result.append(0) + push_warning('change_colors_to_codes: color not found! [%d, %d, %d, %d]' % color) + + image.unlock() + return result + +# makes sure that the color table is at least size 4. +func make_proper_size(color_table: Array) -> Array: + var result := [] + color_table + if color_table.size() < 4: + for i in range(4 - color_table.size()): + result.append([0, 0, 0, 0]) + return result + +func calc_delay_time(frame_delay: float) -> int: + return int(ceil(frame_delay / 0.01)) + +func color_table_to_indexes(colors: Array) -> PoolByteArray: + var result: PoolByteArray = PoolByteArray([]) + for i in range(colors.size()): + result.append(i) + return result + +func add_frame(image: Image, frame_delay: float, quantizator: Script) -> int: + # check if image is of good format + if image.get_format() != Image.FORMAT_RGBA8: + return Error.BAD_IMAGE_FORMAT + + # check if image isn't empty + if image.is_empty(): + return Error.EMPTY_IMAGE + + var found_color_table: Dictionary = find_color_table(image) + + var image_converted_to_codes: PoolByteArray + var transparency_color_index: int = -1 + var color_table: Array + if found_color_table.size() <= 256: # we don't need to quantize the image. + # try to find transparency color index. + transparency_color_index = find_transparency_color_index(found_color_table) + # if didn't found transparency color index but there is atleast one + # place for this color then add it artificially. + if transparency_color_index == -1 and found_color_table.size() <= 255: + found_color_table[[0, 0, 0, 0]] = found_color_table.size() + transparency_color_index = found_color_table.size() - 1 + image_converted_to_codes = change_colors_to_codes( + image, found_color_table, transparency_color_index) + color_table = make_proper_size(found_color_table.keys()) + else: # we have to quantize the image. + var quantization_result: Array = quantizator.new().quantize(image) + image_converted_to_codes = quantization_result[0] + color_table = quantization_result[1] + # transparency index should always be as the first element of color table. + transparency_color_index = 0 if quantization_result[2] else -1 + + last_color_table = color_table + last_transparency_index = transparency_color_index + + var delay_time := calc_delay_time(frame_delay) + + var color_table_indexes := color_table_to_indexes(color_table) + var compressed_image_result: Array = lzw.compress_lzw( + image_converted_to_codes, + color_table_indexes) + var compressed_image_data: PoolByteArray = compressed_image_result[0] + var lzw_min_code_size: int = compressed_image_result[1] + + add_graphic_constrol_ext(delay_time, transparency_color_index) + add_image_descriptor( + Vector2.ZERO, + image.get_size(), + color_table_bit_size(color_table)) + add_local_color_table(color_table) + add_image_data_block(lzw_min_code_size, compressed_image_data) + + return Error.OK + +# adds frame with last color informations +func add_frame_with_lci(image: Image, frame_delay: float) -> int: + # check if image is of good format + if image.get_format() != Image.FORMAT_RGBA8: + return Error.BAD_IMAGE_FORMAT + + # check if image isn't empty + if image.is_empty(): + return Error.EMPTY_IMAGE + + var image_converted_to_codes: PoolByteArray = converter.new().get_similar_indexed_datas(image, last_color_table) + + var color_table_indexes := color_table_to_indexes(last_color_table) + var compressed_image_result: Array = lzw.compress_lzw( + image_converted_to_codes, + color_table_indexes) + var compressed_image_data: PoolByteArray = compressed_image_result[0] + var lzw_min_code_size: int = compressed_image_result[1] + + var delay_time := calc_delay_time(frame_delay) + + add_graphic_constrol_ext(delay_time, last_transparency_index) + add_image_descriptor( + Vector2.ZERO, + image.get_size(), + color_table_bit_size(last_color_table)) + add_local_color_table(last_color_table) + add_image_data_block(lzw_min_code_size, compressed_image_data) + + return Error.OK + +func add_graphic_constrol_ext(_delay_time: float, tci: int = -1) -> void: + var extension_introducer: int = 0x21 + var graphic_control_label: int = 0xf9 + + var block_size: int = 4 + var packed_fields: int = 0b00001000 + if tci != -1: + packed_fields = 0b00001001 + + var delay_time: int = _delay_time + var transparent_color_index: int = tci if tci != -1 else 0 + + data.append(extension_introducer) + data.append(graphic_control_label) + + data.append(block_size) + data.append(packed_fields) + data += little_endian.int_to_2bytes(delay_time) + data.append(transparent_color_index) + + data.append(0) + +func add_image_descriptor(pos: Vector2, + size: Vector2, + l_color_table_size: int) -> void: + var image_separator: int = 0x2c + var packed_fields: int = 0b10000000 | (0b111 & l_color_table_size) + + var little_endian = preload('./little_endian.gd').new() + + data.append(image_separator) + data += little_endian.int_to_2bytes(int(pos.x)) # left pos + data += little_endian.int_to_2bytes(int(pos.y)) # top pos + data += little_endian.int_to_2bytes(int(size.x)) # width + data += little_endian.int_to_2bytes(int(size.y)) # height + data.append(packed_fields) + +func color_table_bit_size(color_table: Array) -> int: + if color_table.size() <= 1: + return 0 + var bit_size := int(ceil(log(color_table.size()) / log(2.0))) + return bit_size - 1 + +func add_local_color_table(color_table: Array) -> void: + for color in color_table: + data.append(color[0]) + data.append(color[1]) + data.append(color[2]) + + var size := color_table_bit_size(color_table) + var proper_size := int(pow(2, size + 1)) + + if color_table.size() != proper_size: + for i in range(proper_size - color_table.size()): + data += PoolByteArray([0, 0, 0]) + +func add_image_data_block(lzw_min_code_size: int, _data: PoolByteArray) -> void: + data.append(lzw_min_code_size) + + var block_size_index: int = 0 + var i: int = 0 + var data_index: int = 0 + while data_index < _data.size(): + if i == 0: + data.append(0) + block_size_index = data.size() - 1 + data.append(_data[data_index]) + data[block_size_index] += 1 + data_index += 1 + i += 1 + if i == 254: + i = 0 + + if not _data.empty(): + data.append(0) diff --git a/addons/gdgifexporter/gifexporter.gd b/addons/gdgifexporter/gifexporter.gd deleted file mode 100644 index 426d8f3d3..000000000 --- a/addons/gdgifexporter/gifexporter.gd +++ /dev/null @@ -1,392 +0,0 @@ -extends Reference - - -var little_endian = preload('./little_endian.gd').new() -var lzw = preload('./gif-lzw/lzw.gd').new() -var used_proc_count: int = 4 - - -class GraphicControlExtension: - var extension_introducer: int = 0x21 - var graphic_control_label: int = 0xf9 - - var block_size: int = 4 - var packed_fields: int = 0b00001000 - var delay_time: int = 0 - var transparent_color_index: int = 0 - - func _init(_delay_time: int, - use_transparency: bool = false, - _transparent_color_index: int = 0): - delay_time = _delay_time - transparent_color_index = _transparent_color_index - if use_transparency: - packed_fields = 0b00001001 - - func to_bytes() -> PoolByteArray: - var little_endian = preload('./little_endian.gd').new() - var result: PoolByteArray = PoolByteArray([]) - - result.append(extension_introducer) - result.append(graphic_control_label) - - result.append(block_size) - result.append(packed_fields) - result += little_endian.int_to_2bytes(delay_time) - result.append(transparent_color_index) - - result.append(0) - - return result - -class ImageDescriptor: - var image_separator: int = 0x2c - var image_left_position: int = 0 - var image_top_position: int = 0 - var image_width: int - var image_height: int - var packed_fields: int = 0b10000000 - - func _init(_image_left_position: int, - _image_top_position: int, - _image_width: int, - _image_height: int, - size_of_local_color_table: int): - image_left_position = _image_left_position - image_top_position = _image_top_position - image_width = _image_width - image_height = _image_height - packed_fields = packed_fields | (0b111 & size_of_local_color_table) - - func to_bytes() -> PoolByteArray: - var little_endian = preload('./little_endian.gd').new() - var result: PoolByteArray = PoolByteArray([]) - - result.append(image_separator) - result += little_endian.int_to_2bytes(image_left_position) - result += little_endian.int_to_2bytes(image_top_position) - result += little_endian.int_to_2bytes(image_width) - result += little_endian.int_to_2bytes(image_height) - result.append(packed_fields) - - return result - -class LocalColorTable: - var colors: Array = [] - - func log2(value: float) -> float: - return log(value) / log(2.0) - - func get_size() -> int: - if colors.size() <= 1: - return 0 - return int(ceil(log2(colors.size()) - 1)) - - func to_bytes() -> PoolByteArray: - var result: PoolByteArray = PoolByteArray([]) - - for v in colors: - result.append(v[0]) - result.append(v[1]) - result.append(v[2]) - - if colors.size() != int(pow(2, get_size() + 1)): - for i in range(int(pow(2, get_size() + 1)) - colors.size()): - result += PoolByteArray([0, 0, 0]) - - return result - -class ApplicationExtension: - var extension_introducer: int = 0x21 - var extension_label: int = 0xff - - var block_size: int = 11 - var application_identifier: PoolByteArray - var appl_authentication_code: PoolByteArray - - var application_data: PoolByteArray - - func _init(_application_identifier: String, - _appl_authentication_code: String): - application_identifier = _application_identifier.to_ascii() - appl_authentication_code = _appl_authentication_code.to_ascii() - - func to_bytes() -> PoolByteArray: - var result: PoolByteArray = PoolByteArray([]) - - result.append(extension_introducer) - result.append(extension_label) - result.append(block_size) - result += application_identifier - result += appl_authentication_code - - result.append(application_data.size()) - result += application_data - - result.append(0) - - return result - -class ImageData: - var lzw_minimum_code_size: int - var image_data: PoolByteArray - - func to_bytes() -> PoolByteArray: - var result: PoolByteArray = PoolByteArray([]) - result.append(lzw_minimum_code_size) - - var block_size_index: int = 0 - var i: int = 0 - var data_index: int = 0 - while data_index < image_data.size(): - if i == 0: - result.append(0) - block_size_index = result.size() - 1 - result.append(image_data[data_index]) - result[block_size_index] += 1 - data_index += 1 - i += 1 - if i == 254: - i = 0 - - if not image_data.empty(): - result.append(0) - - return result - -class ConvertedImage: - var image_converted_to_codes: PoolByteArray - var color_table: Array - var transparency_color_index: int - var width: int - var height: int - -class ConvertionResult: - var converted_image: ConvertedImage = ConvertedImage.new() - var error: int = Error.OK - - func with_error_code(_error: int) -> ConvertionResult: - error = _error - return self - -class ThreadWriteFrameResult: - var frame_data: PoolByteArray = PoolByteArray([]) - var error: int = Error.OK - - func with_error_code(_error: int) -> ThreadWriteFrameResult: - error = _error - return self - -enum Error { - OK = 0, - EMPTY_IMAGE = 1, - BAD_IMAGE_FORMAT = 2 -} - -# File data and Header -var data: PoolByteArray = 'GIF'.to_ascii() + '89a'.to_ascii() - -func _init(_width: int, _height: int): - # Logical Screen Descriptor - var width: int = _width - var height: int = _height - # not Global Color Table Flag - # Color Resolution = 8 bits - # Sort Flag = 0, not sorted. - # Size of Global Color Table set to 0 - # because we'll use only Local Tables - var packed_fields: int = 0b01110000 - var background_color_index: int = 0 - var pixel_aspect_ratio: int = 0 - - data += little_endian.int_to_2bytes(width) - data += little_endian.int_to_2bytes(height) - data.append(packed_fields) - data.append(background_color_index) - data.append(pixel_aspect_ratio) - - var application_extension: ApplicationExtension = ApplicationExtension.new( - "NETSCAPE", - "2.0") - application_extension.application_data = PoolByteArray([1, 0, 0]) - data += application_extension.to_bytes() - -func calc_delay_time(frame_delay: float) -> int: - return int(ceil(frame_delay / 0.01)) - -func color_table_to_indexes(colors: Array) -> PoolByteArray: - var result: PoolByteArray = PoolByteArray([]) - for i in range(colors.size()): - result.append(i) - return result - -func find_color_table_if_has_less_than_256_colors(image: Image) -> Dictionary: - image.lock() - var result: Dictionary = {} - var image_data: PoolByteArray = image.get_data() - - for i in range(0, image_data.size(), 4): - var color: Array = [int(image_data[i]), int(image_data[i + 1]), int(image_data[i + 2]), int(image_data[i + 3])] - if not color in result: - result[color] = result.size() - if result.size() > 256: - break - - image.unlock() - return result - -func change_colors_to_codes(image: Image, - color_palette: Dictionary, - transparency_color_index: int) -> PoolByteArray: - image.lock() - var image_data: PoolByteArray = image.get_data() - var result: PoolByteArray = PoolByteArray([]) - - for i in range(0, image_data.size(), 4): - var color: Array = [int(image_data[i]), int(image_data[i + 1]), int(image_data[i + 2]), int(image_data[i + 3])] - if color in color_palette: - if color[3] == 0 and transparency_color_index != -1: - result.append(transparency_color_index) - else: - result.append(color_palette[color]) - else: - result.append(0) - push_warning('change_colors_to_codes: color not found! [%d, %d, %d, %d]' % color) - - image.unlock() - return result - -func sum_color(color: Array) -> int: - return color[0] + color[1] + color[2] + color[3] - -func find_transparency_color_index(color_table: Dictionary) -> int: - for color in color_table: - if sum_color(color) == 0: - return color_table[color] - return -1 - -func find_transparency_color_index_for_quantized_image(color_table: Array) -> int: - for i in range(color_table.size()): - if sum_color(color_table[i]) == 0: - return i - return -1 - -func make_sure_color_table_is_at_least_size_4(color_table: Array) -> Array: - var result := [] + color_table - if color_table.size() < 4: - for i in range(4 - color_table.size()): - result.append([0, 0, 0, 0]) - return result - -func convert_image(image: Image, quantizator) -> ConvertionResult: - var result := ConvertionResult.new() - - # check if image is of good format - if image.get_format() != Image.FORMAT_RGBA8: - return result.with_error_code(Error.BAD_IMAGE_FORMAT) - - # check if image isn't empty - if image.is_empty(): - return result.with_error_code(Error.EMPTY_IMAGE) - - var found_color_table: Dictionary = find_color_table_if_has_less_than_256_colors( - image) - - var image_converted_to_codes: PoolByteArray - var transparency_color_index: int = -1 - var color_table: Array - if found_color_table.size() <= 256: # we don't need to quantize the image. - # exporter images always try to include transparency because I'm lazy. - transparency_color_index = find_transparency_color_index(found_color_table) - if transparency_color_index == -1 and found_color_table.size() <= 255: - found_color_table[[0, 0, 0, 0]] = found_color_table.size() - transparency_color_index = found_color_table.size() - 1 - image_converted_to_codes = change_colors_to_codes( - image, found_color_table, transparency_color_index) - color_table = make_sure_color_table_is_at_least_size_4(found_color_table.keys()) - else: # we have to quantize the image. - var quantization_result: Array = quantizator.quantize_and_convert_to_codes(image) - image_converted_to_codes = quantization_result[0] - color_table = quantization_result[1] - # don't find transparency index if the quantization algorithm - # provides it as third return value - if quantization_result.size() == 3: - transparency_color_index = 0 if quantization_result[2] else -1 - else: - transparency_color_index = find_transparency_color_index_for_quantized_image(quantization_result[1]) - - result.converted_image.image_converted_to_codes = image_converted_to_codes - result.converted_image.color_table = color_table - result.converted_image.transparency_color_index = transparency_color_index - result.converted_image.width = image.get_width() - result.converted_image.height = image.get_height() - - return result.with_error_code(Error.OK) - -func write_frame(image: Image, frame_delay: float, quantizator) -> int: - var converted_image_result := convert_image(image, quantizator) - if converted_image_result.error != Error.OK: - return converted_image_result.error - - var converted_image := converted_image_result.converted_image - return write_frame_from_conv_image(converted_image, frame_delay) - -func write_frame_from_conv_image(converted_image: ConvertedImage, - frame_delay: float) -> int: - var delay_time := calc_delay_time(frame_delay) - - var color_table_indexes = color_table_to_indexes(converted_image.color_table) - var compressed_image_result: Array = lzw.compress_lzw( - converted_image.image_converted_to_codes, color_table_indexes) - var compressed_image_data: PoolByteArray = compressed_image_result[0] - var lzw_min_code_size: int = compressed_image_result[1] - - var table_image_data_block: ImageData = ImageData.new() - table_image_data_block.lzw_minimum_code_size = lzw_min_code_size - table_image_data_block.image_data = compressed_image_data - - var local_color_table: LocalColorTable = LocalColorTable.new() - local_color_table.colors = converted_image.color_table - - var image_descriptor: ImageDescriptor = ImageDescriptor.new(0, 0, - converted_image.width, - converted_image.height, - local_color_table.get_size()) - - var graphic_control_extension: GraphicControlExtension - if converted_image.transparency_color_index != -1: - graphic_control_extension = GraphicControlExtension.new( - delay_time, true, converted_image.transparency_color_index) - else: - graphic_control_extension = GraphicControlExtension.new( - delay_time, false, 0) - - data += graphic_control_extension.to_bytes() - data += image_descriptor.to_bytes() - data += local_color_table.to_bytes() - data += table_image_data_block.to_bytes() - - return Error.OK - -func scale_conv_image(converted_image: ConvertedImage, scale_factor: int) -> ConvertedImage: - var result = ConvertedImage.new() - - result.image_converted_to_codes = PoolByteArray([]) - result.color_table = converted_image.color_table.duplicate() - result.transparency_color_index = converted_image.transparency_color_index - result.width = converted_image.width * scale_factor - result.height = converted_image.height * scale_factor - - for y in range(converted_image.height): - var row := PoolByteArray([]) - for x in range(converted_image.width): - for i in range(scale_factor): - row.append(converted_image.image_converted_to_codes[(y * converted_image.width) + x]) - for i in range(scale_factor): - result.image_converted_to_codes += row - row = PoolByteArray([]) - - return result - -func export_file_data() -> PoolByteArray: - return data + PoolByteArray([0x3b]) diff --git a/addons/gdgifexporter/quantization/median_cut.gd b/addons/gdgifexporter/quantization/median_cut.gd index 98fac2bb1..36135b408 100644 --- a/addons/gdgifexporter/quantization/median_cut.gd +++ b/addons/gdgifexporter/quantization/median_cut.gd @@ -2,148 +2,155 @@ extends Reference var converter = preload('../converter.gd').new() -var color_table: Dictionary = {} -var transparency: bool = false -var tree: TreeNode -var leaf: Array = [] +var transparency := false - -class TreeNode: - var colors: Array - var average_color: Array - var axis: int - var median: int - # Comments is workaround for Godot memory leak bug - var parent#: TreeNode - var left#: TreeNode - var right#: TreeNode - - - func _init(_parent: TreeNode, _colors: Array): - self.parent = _parent - self.colors = _colors - - - func median_cut() -> void: - var start: Array = [255, 255, 255] - var end: Array = [0, 0, 0] - var delta: Array = [0, 0, 0] - - for color in colors: - for i in 3: - if color[i] < start[i]: - start[i] = color[i] - if color[i] > end[i]: - end[i] = color[i] +func longest_axis(colors: Array) -> int: + var start := [255, 255, 255] + var end := [0, 0, 0] + for color in colors: for i in 3: - delta[i] = end[i] - start[i] - axis = 0 - if delta[1] > delta[0]: - axis = 1 - if delta[2] > delta[axis]: - axis = 2 + start[i] = min(color[i], start[i]) + end[i] = max(color[i], end[i]) + + var max_r = end[0] - start[0] + var max_g = end[1] - start[1] + var max_b = end[2] - start[2] + + if max_r > max_g: + if max_r > max_b: + return 0 + else: + if max_g > max_b: + return 1 + return 2 - var axis_sort: Array = [] - for i in colors.size(): - axis_sort.append(colors[i][axis]) - axis_sort.sort() - var cut = colors.size() >> 1 - median = axis_sort[cut] - - var left_colors: Array = [] - var right_colors: Array = [] - for color in colors: - if color[axis] < median: - left_colors.append(color) - else: - right_colors.append(color) - left = TreeNode.new(self, left_colors) - right = TreeNode.new(self, right_colors) - colors = [] +func get_median(colors: Array) -> Vector3: + return colors[colors.size() >> 1] - func calculate_average_color(color_table: Dictionary) -> void: - average_color = [0, 0, 0] - var total: int = 0 - for color in colors: - var weight = color_table[color] - for i in 3: - average_color[i] += color[i] * weight - total += weight - for i in 3: - average_color[i] /= total +func median_cut(colors: Array) -> Array: + var axis := longest_axis(colors) + + var axis_sort := [] + for color in colors: + axis_sort.append(color[axis]) + axis_sort.sort() + + var cut := axis_sort.size() >> 1 + var median: int = axis_sort[cut] + axis_sort = [] + + var left_colors := [] + var right_colors := [] + for color in colors: + if color[axis] < median: + left_colors.append(color) + else: + right_colors.append(color) -func fill_color_table(image: Image) -> void: + return [left_colors, right_colors] + +func average_color(bucket: Array) -> Array: + var r := 0 + var g := 0 + var b := 0 + for color in bucket: + r += color[0] + g += color[1] + b += color[2] + return [r / bucket.size(), g / bucket.size(), b / bucket.size()] + +func average_colors(buckets: Array) -> Dictionary: + var avg_colors := {} + for bucket in buckets: + if bucket.size() > 0: + avg_colors[average_color(bucket)] = avg_colors.size() + return avg_colors + +func pixels_to_colors(image: Image) -> Array: image.lock() + var result := [] var data: PoolByteArray = image.get_data() for i in range(0, data.size(), 4): if data[i + 3] == 0: transparency = true continue - var color: Array = [data[i], data[i + 1], data[i + 2]] - var count = color_table.get(color, 0) - color_table[color] = count + 1 - image.unlock() - - -func convert_image(image: Image, colors: Array) -> PoolByteArray: - image.lock() - var data: PoolByteArray = image.get_data() - var nearest_lookup: Dictionary = {} - var result: PoolByteArray = PoolByteArray() - - for i in colors.size(): - colors[i] = Vector3(colors[i][0], colors[i][1], colors[i][2]) - - for i in range(0, data.size(), 4): - if data[i + 3] == 0: - result.append(0) - continue - var current: Vector3 = Vector3(data[i], data[i + 1], data[i + 2]) - var nearest_index: int = 0 + int(transparency) - if current in nearest_lookup: - nearest_index = nearest_lookup[current] - else: - var nearest_distance: float = current.distance_squared_to(colors[nearest_index]) - for j in range(1 + int(transparency), colors.size()): - var distance: float = current.distance_squared_to(colors[j]) - if distance < nearest_distance: - nearest_index = j - nearest_distance = distance - nearest_lookup[current] = nearest_index - result.append(nearest_index) - + result.append([data[i], data[i + 1], data[i + 2]]) image.unlock() return result +func remove_smallest_bucket(buckets: Array) -> Array: + if buckets.size() == 0: + return buckets + var i_of_smallest_bucket := 0 + for i in range(buckets.size()): + if buckets[i].size() < buckets[i_of_smallest_bucket].size(): + i_of_smallest_bucket = i + buckets.remove(i_of_smallest_bucket) + return buckets -func quantize_and_convert_to_codes(image: Image) -> Array: - color_table.clear() - transparency = false - fill_color_table(image) +func remove_empty_buckets(buckets: Array) -> Array: + if buckets.size() == 0: + return buckets + + var i := buckets.find([]) + while i != -1: + buckets.remove(i) + i = buckets.find([]) + + return buckets - tree = TreeNode.new(null, color_table.keys()) - leaf = [tree] - var num = 254 if transparency else 255 - while leaf.size() <= num: - var node = leaf.pop_front() - if node.colors.size() > 1: - node.median_cut() - leaf.append(node.left) - leaf.append(node.right) - if leaf.size() <= 0: - break +# quantizes to gif ready codes +func quantize(image: Image) -> Array: + var pixels = pixels_to_colors(image) + if pixels.size() == 0: + return pixels - var color_quantized: Dictionary = {} - for node in leaf: - node.calculate_average_color(color_table) - color_quantized[node.average_color] = color_quantized.size() + var buckets := [pixels] + var done_buckets := [] - var color_array: Array = color_quantized.keys() + # it tells how many times buckets should be divided into two + var dimensions := 8 + + for i in range(0, dimensions): + var new_buckets := [] + for bucket in buckets: + # don't median cut if bucket is smaller than 2, because + # it won't produce two new buckets. + if bucket.size() > 1: + var res := median_cut(bucket) + # sometimes when you try to median cut a bucket, the result + # is one with size equal to 0 and other with full size as the + # source bucket. Because of that it's useless to try to divide + # it further so it's better to put it into separate list and + # process only those buckets witch divide further. + if res[0].size() == 0 or res[1].size() == 0: + done_buckets += res + else: + new_buckets += res + buckets = [] + buckets = new_buckets + + var all_buckets := remove_empty_buckets(done_buckets + buckets) + + buckets = [] + done_buckets = [] + if transparency: - color_array.push_front([0, 0, 0]) + if all_buckets.size() == pow(2, dimensions): + all_buckets = remove_smallest_bucket(all_buckets) + + # dictionaries are only for speed. + var color_array := average_colors(all_buckets).keys() + + # if pixel_to_colors detected that the image has transparent pixels + # then add transparency color at the beginning so it will be properly + # exported. + if transparency: + color_array = [[0, 0, 0]] + color_array var data: PoolByteArray = converter.get_similar_indexed_datas(image, color_array) + return [data, color_array, transparency] diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index e41884795..e650b4fec 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -1,8 +1,8 @@ extends Node # Gif exporter -const gifexporter = preload("res://addons/gdgifexporter/gifexporter.gd") -var quantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd").new() +const GIFExporter = preload("res://addons/gdgifexporter/exporter.gd") +const MedianCutQuantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd") enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 } var current_tab : int = ExportTab.FRAME @@ -212,7 +212,7 @@ func export_gif(args: Dictionary) -> void: args["export_dialog"].toggle_export_progress_popup(true) # Export and save gif - var exporter = gifexporter.new(processed_images[0].get_width(), processed_images[0].get_height()) + var exporter = GIFExporter.new(processed_images[0].get_width(), processed_images[0].get_height()) match direction: AnimationDirection.FORWARD: for i in range(processed_images.size()): @@ -240,7 +240,7 @@ func export_gif(args: Dictionary) -> void: func write_frame_to_gif(image: Image, wait_time: float, exporter: Reference, export_dialog: Node) -> void: - exporter.write_frame(image, wait_time, quantization) + exporter.add_frame(image, wait_time, MedianCutQuantization) increase_export_progress(export_dialog)