From 227095a4f710ac6afd43f0a7e8b296f188cf20be Mon Sep 17 00:00:00 2001
From: David Luevano Alvarado <david@luevano.xyz>
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**<code size>
+	# <code size> 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
+	# <LOOP POINT>
+	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
+	# <LOOP POINT>
+	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