From 227095a4f710ac6afd43f0a7e8b296f188cf20be Mon Sep 17 00:00:00 2001 From: David Luevano Alvarado Date: Tue, 31 May 2022 21:11:46 -0600 Subject: add working gif maker --- .../godot-gdgifexporter/gdgifexporter/LICENSE | 21 ++ .../godot-gdgifexporter/gdgifexporter/converter.gd | 59 +++++ .../godot-gdgifexporter/gdgifexporter/exporter.gd | 292 +++++++++++++++++++++ .../gdgifexporter/gif-lzw/LICENSE | 21 ++ .../gdgifexporter/gif-lzw/lsbbitpacker.gd | 31 +++ .../gdgifexporter/gif-lzw/lsbbitunpacker.gd | 41 +++ .../gdgifexporter/gif-lzw/lzw.gd | 210 +++++++++++++++ .../gdgifexporter/little_endian.gd | 5 + .../gdgifexporter/lookup_color.shader | 19 ++ .../gdgifexporter/lookup_similar.shader | 22 ++ .../gdgifexporter/quantization/median_cut.gd | 163 ++++++++++++ .../gdgifexporter/quantization/uniform.gd | 88 +++++++ 12 files changed, 972 insertions(+) create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd create mode 100644 src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd (limited to 'src/addons/GifMaker/godot-gdgifexporter/gdgifexporter') diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE new file mode 100644 index 0000000..ca5b2ad --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Igor Santarek + +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. diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd new file mode 100644 index 0000000..846d93c --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/converter.gd @@ -0,0 +1,59 @@ +extends Reference + +var _shader: Shader + + +func get_indexed_datas(image: Image, colors: Array) -> PoolByteArray: + _shader = preload("./lookup_color.shader") + return _convert(image, colors) + + +func get_similar_indexed_datas(image: Image, colors: Array) -> PoolByteArray: + _shader = preload("./lookup_similar.shader") + return _convert(image, colors) + + +func _convert(image: Image, colors: Array) -> PoolByteArray: + var vp = VisualServer.viewport_create() + var canvas = VisualServer.canvas_create() + VisualServer.viewport_attach_canvas(vp, canvas) + VisualServer.viewport_set_size(vp, image.get_width(), image.get_height()) + VisualServer.viewport_set_disable_3d(vp, true) + VisualServer.viewport_set_usage(vp, VisualServer.VIEWPORT_USAGE_2D) + VisualServer.viewport_set_hdr(vp, true) + VisualServer.viewport_set_active(vp, true) + + var ci_rid = VisualServer.canvas_item_create() + VisualServer.viewport_set_canvas_transform(vp, canvas, Transform()) + VisualServer.canvas_item_set_parent(ci_rid, canvas) + var texture = ImageTexture.new() + texture.create_from_image(image) + VisualServer.canvas_item_add_texture_rect( + ci_rid, Rect2(Vector2(0, 0), image.get_size()), texture + ) + + var mat_rid = VisualServer.material_create() + VisualServer.material_set_shader(mat_rid, _shader.get_rid()) + var lut = Image.new() + lut.create(256, 1, false, Image.FORMAT_RGB8) + lut.fill(Color8(colors[0][0], colors[0][1], colors[0][2])) + lut.lock() + for i in colors.size(): + lut.set_pixel(i, 0, Color8(colors[i][0], colors[i][1], colors[i][2])) + var lut_tex = ImageTexture.new() + lut_tex.create_from_image(lut) + VisualServer.material_set_param(mat_rid, "lut", lut_tex) + VisualServer.canvas_item_set_material(ci_rid, mat_rid) + + VisualServer.viewport_set_update_mode(vp, VisualServer.VIEWPORT_UPDATE_ONCE) + VisualServer.viewport_set_vflip(vp, true) + VisualServer.force_draw(false) + image = VisualServer.texture_get_data(VisualServer.viewport_get_texture(vp)) + + VisualServer.free_rid(vp) + VisualServer.free_rid(canvas) + VisualServer.free_rid(ci_rid) + VisualServer.free_rid(mat_rid) + + image.convert(Image.FORMAT_R8) + return image.get_data() diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/exporter.gd new file mode 100644 index 0000000..eb33c27 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/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) + +func add_comment_ext(string: String) -> void: + var extension_introducer := 0x21 + var comment_label := 0xFE + + data.append(extension_introducer) + data.append(comment_label) + data += string.to_utf8() + 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 colors_to_codes(img: Image, col_palette: Dictionary, transp_color_index: int) -> PoolByteArray: + img.lock() + var image_data: PoolByteArray = img.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 col_palette: + if color[3] == 0 and transp_color_index != -1: + result.append(transp_color_index) + else: + result.append(col_palette[color]) + else: + result.append(0) + push_warning("colors_to_codes: color not found! [%d, %d, %d, %d]" % color) + + img.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 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, delay_time: int, 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 = 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 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, delay_time: int) -> 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] + + 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) + + 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/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE new file mode 100644 index 0000000..ca5b2ad --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Igor Santarek + +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. diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd new file mode 100644 index 0000000..3141d09 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitpacker.gd @@ -0,0 +1,31 @@ +extends Reference + + +class LSBLZWBitPacker: + var bit_index: int = 0 + var stream: int = 0 + + var chunks: PoolByteArray = PoolByteArray([]) + + func put_byte(): + chunks.append(stream & 0xff) + bit_index -= 8 + stream >>= 8 + + func write_bits(value: int, bits_count: int) -> void: + value &= (1 << bits_count) - 1 + value <<= bit_index + stream |= value + bit_index += bits_count + while bit_index >= 8: + self.put_byte() + + func pack() -> PoolByteArray: + if bit_index != 0: + self.put_byte() + return chunks + + func reset() -> void: + bit_index = 0 + stream = 0 + chunks = PoolByteArray([]) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd new file mode 100644 index 0000000..9f8507f --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lsbbitunpacker.gd @@ -0,0 +1,41 @@ +extends Reference + + +class LSBLZWBitUnpacker: + var chunk_stream: PoolByteArray + var bit_index: int = 0 + var byte: int + var byte_index: int = 0 + + func _init(_chunk_stream: PoolByteArray): + chunk_stream = _chunk_stream + self.get_byte() + + func get_bit(value: int, index: int) -> int: + return (value >> index) & 1 + + func set_bit(value: int, index: int) -> int: + return value | (1 << index) + + func get_byte(): + byte = chunk_stream[byte_index] + byte_index += 1 + bit_index = 0 + + func read_bits(bits_count: int) -> int: + var result: int = 0 + var result_bit_index: int = 0 + + for _i in range(bits_count): + if self.get_bit(byte, bit_index) == 1: + result = self.set_bit(result, result_bit_index) + result_bit_index += 1 + bit_index += 1 + + if bit_index == 8: + self.get_byte() + + return result + + func remove_bits(bits_count: int) -> void: + self.read_bits(bits_count) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd new file mode 100644 index 0000000..9140e7c --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/gif-lzw/lzw.gd @@ -0,0 +1,210 @@ +extends Reference + +var lsbbitpacker = preload("./lsbbitpacker.gd") +var lsbbitunpacker = preload("./lsbbitunpacker.gd") + + +class CodeEntry: + var sequence: PoolByteArray + var raw_array: Array + + func _init(_sequence): + raw_array = _sequence + sequence = _sequence + + func add(other): + return CodeEntry.new(self.raw_array + other.raw_array) + + func to_string(): + var result: String = "" + for element in self.sequence: + result += str(element) + ", " + return result.substr(0, result.length() - 2) + + +class CodeTable: + var entries: Dictionary = {} + var counter: int = 0 + var lookup: Dictionary = {} + + func add(entry) -> int: + self.entries[self.counter] = entry + self.lookup[entry.raw_array] = self.counter + counter += 1 + return counter + + func find(entry) -> int: + return self.lookup.get(entry.raw_array, -1) + + func has(entry) -> bool: + return self.find(entry) != -1 + + func get(index) -> CodeEntry: + return self.entries.get(index, null) + + func to_string() -> String: + var result: String = "CodeTable:\n" + for id in self.entries: + result += str(id) + ": " + self.entries[id].to_string() + "\n" + result += "Counter: " + str(self.counter) + "\n" + return result + + +func log2(value: float) -> float: + return log(value) / log(2.0) + + +func get_bits_number_for(value: int) -> int: + if value == 0: + return 1 + return int(ceil(log2(value + 1))) + + +func initialize_color_code_table(colors: PoolByteArray) -> CodeTable: + var result_code_table: CodeTable = CodeTable.new() + for color_id in colors: + # warning-ignore:return_value_discarded + result_code_table.add(CodeEntry.new([color_id])) + # move counter to the first available compression code index + var last_color_index: int = colors.size() - 1 + var clear_code_index: int = pow(2, get_bits_number_for(last_color_index)) + result_code_table.counter = clear_code_index + 2 + return result_code_table + + +# compression and decompression done with source: +# http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp + + +func compress_lzw(image: PoolByteArray, colors: PoolByteArray) -> Array: + # Initialize code table + var code_table: CodeTable = initialize_color_code_table(colors) + # Clear Code index is 2** + # is the amount of bits needed to write down all colors + # from color table. We use last color index because we can write + # all colors (for example 16 colors) with indexes from 0 to 15. + # Number 15 is in binary 0b1111, so we'll need 4 bits to write all + # colors down. + var last_color_index: int = colors.size() - 1 + var clear_code_index: int = pow(2, get_bits_number_for(last_color_index)) + var index_stream: PoolByteArray = image + var current_code_size: int = get_bits_number_for(clear_code_index) + var binary_code_stream = lsbbitpacker.LSBLZWBitPacker.new() + + # initialize with Clear Code + binary_code_stream.write_bits(clear_code_index, current_code_size) + + # Read first index from index stream. + var index_buffer: CodeEntry = CodeEntry.new([index_stream[0]]) + var data_index: int = 1 + # + while data_index < index_stream.size(): + # Get the next index from the index stream. + var k: CodeEntry = CodeEntry.new([index_stream[data_index]]) + data_index += 1 + # Is index buffer + k in our code table? + var new_index_buffer: CodeEntry = index_buffer.add(k) + if code_table.has(new_index_buffer): # if YES + # Add k to the end of the index buffer + index_buffer = new_index_buffer + else: # if NO + # Add a row for index buffer + k into our code table + binary_code_stream.write_bits(code_table.find(index_buffer), current_code_size) + + # We don't want to add new code to code table if we've exceeded 4095 + # index. + var last_entry_index: int = code_table.counter - 1 + if last_entry_index != 4095: + # Output the code for just the index buffer to our code stream + # warning-ignore:return_value_discarded + code_table.add(new_index_buffer) + else: + # if we exceeded 4095 index (code table is full), we should + # output Clear Code and reset everything. + binary_code_stream.write_bits(clear_code_index, current_code_size) + code_table = initialize_color_code_table(colors) + # get_bits_number_for(clear_code_index) is the same as + # LZW code size + 1 + current_code_size = get_bits_number_for(clear_code_index) + + # Detect when you have to save new codes in bigger bits boxes + # change current code size when it happens because we want to save + # flexible code sized codes + var new_code_size_candidate: int = get_bits_number_for(code_table.counter - 1) + if new_code_size_candidate > current_code_size: + current_code_size = new_code_size_candidate + + # Index buffer is set to k + index_buffer = k + # Output code for contents of index buffer + binary_code_stream.write_bits(code_table.find(index_buffer), current_code_size) + + # output end with End Of Information Code + binary_code_stream.write_bits(clear_code_index + 1, current_code_size) + + var min_code_size: int = get_bits_number_for(clear_code_index) - 1 + + return [binary_code_stream.pack(), min_code_size] + + +# gdlint: ignore=max-line-length +func decompress_lzw(code_stream_data: PoolByteArray, min_code_size: int, colors: PoolByteArray) -> PoolByteArray: + var code_table: CodeTable = initialize_color_code_table(colors) + var index_stream: PoolByteArray = PoolByteArray([]) + var binary_code_stream = lsbbitunpacker.LSBLZWBitUnpacker.new(code_stream_data) + var current_code_size: int = min_code_size + 1 + var clear_code_index: int = pow(2, min_code_size) + + # CODE is an index of code table, {CODE} is sequence inside + # code table with index CODE. The same goes for PREVCODE. + + # Remove first Clear Code from stream. We don't need it. + binary_code_stream.remove_bits(current_code_size) + + # let CODE be the first code in the code stream + var code: int = binary_code_stream.read_bits(current_code_size) + # output {CODE} to index stream + index_stream.append_array(code_table.get(code).sequence) + # set PREVCODE = CODE + var prevcode: int = code + # + while true: + # let CODE be the next code in the code stream + code = binary_code_stream.read_bits(current_code_size) + # Detect Clear Code. When detected reset everything and get next code. + if code == clear_code_index: + code_table = initialize_color_code_table(colors) + current_code_size = min_code_size + 1 + code = binary_code_stream.read_bits(current_code_size) + elif code == clear_code_index + 1: # Stop when detected EOI Code. + break + # is CODE in the code table? + var code_entry: CodeEntry = code_table.get(code) + if code_entry != null: # if YES + # output {CODE} to index stream + index_stream.append_array(code_entry.sequence) + # let k be the first index in {CODE} + var k: CodeEntry = CodeEntry.new([code_entry.sequence[0]]) + # warning-ignore:return_value_discarded + # add {PREVCODE} + k to the code table + code_table.add(code_table.get(prevcode).add(k)) + # set PREVCODE = CODE + prevcode = code + else: # if NO + # let k be the first index of {PREVCODE} + var prevcode_entry: CodeEntry = code_table.get(prevcode) + var k: CodeEntry = CodeEntry.new([prevcode_entry.sequence[0]]) + # output {PREVCODE} + k to index stream + index_stream.append_array(prevcode_entry.add(k).sequence) + # add {PREVCODE} + k to code table + # warning-ignore:return_value_discarded + code_table.add(prevcode_entry.add(k)) + # set PREVCODE = CODE + prevcode = code + + # Detect when we should increase current code size and increase it. + var new_code_size_candidate: int = get_bits_number_for(code_table.counter) + if new_code_size_candidate > current_code_size: + current_code_size = new_code_size_candidate + + return index_stream diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd new file mode 100644 index 0000000..cb865e1 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/little_endian.gd @@ -0,0 +1,5 @@ +extends Reference + + +func int_to_2bytes(value: int) -> PoolByteArray: + return PoolByteArray([value & 255, (value >> 8) & 255]) diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader new file mode 100644 index 0000000..8379108 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_color.shader @@ -0,0 +1,19 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform sampler2D lut; + +void fragment() { + vec4 color = texture(TEXTURE, UV); + float index = 0.0; + if (color.a > 0.0) { + for (int i = 0; i < 256; i++) { + vec4 c = texture(lut, vec2((float(i) + 0.5) / 256.0, 0.5)); + if (c.rgb == color.rgb) { + index = float(i) / 255.0; + break; + } + } + } + COLOR = vec4(vec3(index), 1.0); +} \ No newline at end of file diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader new file mode 100644 index 0000000..0614661 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/lookup_similar.shader @@ -0,0 +1,22 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform sampler2D lut; + +void fragment() { + vec4 color = texture(TEXTURE, UV); + vec4 similar = texture(lut, vec2(0.5 / 256.0, 0.5)); + float index = 0.0; + if (color.a > 0.0) { + float dist = distance(color.xyz, similar.xyz); + for (int i = 1; i < 256; i++) { + vec4 c = texture(lut, vec2((float(i) + 0.5) / 256.0, 0.5)); + float d = distance(color.xyz, c.xyz); + if (d < dist) { + dist = d; + index = float(i) / 255.0; + } + } + } + COLOR = vec4(vec3(index), 1.0); +} \ No newline at end of file diff --git a/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd new file mode 100644 index 0000000..4610f03 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/median_cut.gd @@ -0,0 +1,163 @@ +extends Reference + +var converter = preload("../converter.gd").new() +var transparency := false + + +func longest_axis(colors: Array) -> int: + var start := [255, 255, 255] + var end := [0, 0, 0] + for color in colors: + for i in 3: + 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 + + +func get_median(colors: Array) -> Vector3: + return colors[colors.size() >> 1] + + +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) + + 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 + 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 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 + + +# quantizes to gif ready codes +func quantize(image: Image) -> Array: + var pixels = pixels_to_colors(image) + if pixels.size() == 0: + return pixels + + var buckets := [pixels] + var done_buckets := [] + + # 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: + 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/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd new file mode 100644 index 0000000..e953342 --- /dev/null +++ b/src/addons/GifMaker/godot-gdgifexporter/gdgifexporter/quantization/uniform.gd @@ -0,0 +1,88 @@ +extends Reference + +var converter = preload("../converter.gd").new() +var transparency := false + + +func how_many_divisions(colors_count: int) -> int: + return int(ceil(pow(colors_count, 1.0 / 4.0))) + + +func generate_colors(colors_count: int) -> Array: + var divisions_count: int = how_many_divisions(colors_count) + var colors: Array = [] + + for a in range(divisions_count): + for b in range(divisions_count): + for g in range(divisions_count): + for r in range(divisions_count): + colors.append( + [ + Vector3( + (255.0 / divisions_count) * r, + (255.0 / divisions_count) * g, + (255.0 / divisions_count) * b + ), + (255.0 / divisions_count) * a + ] + ) + + return colors + + +func find_nearest_color(palette_color: Vector3, image_data: PoolByteArray) -> Array: + var nearest_color = null + var nearest_alpha = null + for i in range(0, image_data.size(), 4): + var color = Vector3(image_data[i], image_data[i + 1], image_data[i + 2]) + # detect transparency + if image_data[3] == 0: + transparency = true + if ( + (nearest_color == null) + or ( + palette_color.distance_squared_to(color) + < palette_color.distance_squared_to(nearest_color) + ) + ): + nearest_color = color + nearest_alpha = image_data[i + 3] + return [nearest_color, nearest_alpha] + + +# moves every color from palette colors to the nearest found color in image +func enhance_colors(image: Image, palette_colors: Array) -> Array: + var data := image.get_data() + + for i in range(palette_colors.size()): + var nearest_color := find_nearest_color(palette_colors[i][0], data) + palette_colors[i] = nearest_color + + return palette_colors + + +func to_color_array(colors: Array) -> Array: + var result := [] + for v in colors: + result.append([v[0].x, v[0].y, v[0].z]) + return result + + +# quantizes to gif ready codes +func quantize(image: Image) -> Array: + image.lock() + + var colors: Array = generate_colors(256) + var tmp_image: Image = Image.new() + tmp_image.copy_from(image) + tmp_image.resize(32, 32) + tmp_image.lock() + colors = enhance_colors(tmp_image, colors) + tmp_image.unlock() + + image.unlock() + colors = to_color_array(colors) + + var data: PoolByteArray = converter.get_similar_indexed_datas(image, colors) + + return [data, colors, transparency] -- cgit v1.2.3-70-g09d2