1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 09:09:47 +00:00

Replace godot-gifexporter with godot-gdgifexporter (#295)

Add exporting in a separate thread and a progress bar
Remove background color option from gif export
This commit is contained in:
Martin Novák 2020-08-07 07:13:04 +02:00 committed by GitHub
parent e4aa17b01c
commit f3bce3857a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1073 additions and 80 deletions

10
addons/README.md Normal file
View file

@ -0,0 +1,10 @@
# Addons
## gdgifexporter
- Upstream: https://github.com/jegor377/godot-gdgifexporter
- Version: git (9cdc448922717f069dd12e0377c1d9fc09d30f9f, 2020)
- License: MIT
Files extracted from source:
- `gdgifexporter/quantization/enhanced_uniform_quantization.gd`

View file

@ -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.

View file

@ -0,0 +1,39 @@
extends Node
func setup(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 shader = preload("./lookup_similar.shader")
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.lock()
for i in 256:
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))
image.convert(Image.FORMAT_R8)
return image.get_data()

View file

@ -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.

View file

@ -0,0 +1,38 @@
extends Node
class LSB_LZWBitPacker:
var bit_index: int = 0
var byte: int = 0
var chunks: PoolByteArray = PoolByteArray([])
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 put_byte():
chunks.append(byte)
bit_index = 0
byte = 0
func write_bits(value: int, bits_count: int) -> void:
for i in range(bits_count):
if self.get_bit(value, i) == 1:
byte = self.set_bit(byte, bit_index)
bit_index += 1
if bit_index == 8:
self.put_byte()
func pack() -> PoolByteArray:
if bit_index != 0:
self.put_byte()
return chunks
func reset() -> void:
bit_index = 0
byte = 0
chunks = PoolByteArray([])

View file

@ -0,0 +1,41 @@
extends Node
class LSB_LZWBitUnpacker:
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)

View file

@ -0,0 +1,202 @@
extends Node
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.LSB_LZWBitPacker.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]
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.LSB_LZWBitUnpacker.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

View file

@ -0,0 +1,392 @@
extends Node
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])

View file

@ -0,0 +1,5 @@
extends Node
func int_to_2bytes(value: int) -> PoolByteArray:
return PoolByteArray([value & 255, (value >> 8) & 255])

View file

@ -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);
}

View file

@ -0,0 +1,148 @@
extends Node
var converter = preload('../converter.gd').new()
var color_table: Dictionary = {}
var transparency: bool = false
var tree: TreeNode
var leaf: Array = []
class TreeNode:
var colors: Array
var average_color: Array
var axis: int
var median: int
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]
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
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 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 fill_color_table(image: Image) -> void:
image.lock()
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)
image.unlock()
return result
func quantize_and_convert_to_codes(image: Image) -> Array:
color_table.clear()
transparency = false
fill_color_table(image)
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
var color_quantized: Dictionary = {}
for node in leaf:
node.calculate_average_color(color_table)
color_quantized[node.average_color] = color_quantized.size()
var color_array: Array = color_quantized.keys()
if transparency:
color_array.push_front([0, 0, 0])
var data: PoolByteArray = converter.setup(image, color_array)
return [data, color_array, transparency]

@ -1 +0,0 @@
Subproject commit 1af82adeafb98a0cfc8eaad5a02efe9774242947

View file

@ -129,10 +129,6 @@ gdscript/warnings/return_value_discarded=false
window/size/width=1280
window/size/height=720
[editor_plugins]
enabled=PoolStringArray( "godot-gifexporter" )
[importer_defaults]
texture={

View file

@ -1,5 +1,9 @@
extends Node
# Gif exporter
const gifexporter = preload("res://addons/gdgifexporter/gifexporter.gd")
var quantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd").new()
enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 }
var current_tab : int = ExportTab.FRAME
@ -20,7 +24,6 @@ var lines_count := 1
# Animation options
enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 }
var animation_type : int = AnimationType.MULTIPLE_FILES
var background_color : Color = Color.white
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
var direction : int = AnimationDirection.FORWARD
@ -43,7 +46,6 @@ var exported_frame_current_tag : int
var exported_orientation : int
var exported_lines_count : int
var exported_animation_type : int
var exported_background_color : Color
var exported_direction : int
var exported_resize : int
var exported_interpolation : int
@ -56,6 +58,16 @@ var stop_export = false
var file_exists_alert = "File %s already exists. Overwrite?"
# Export progress variables
var export_progress_fraction := 0.0
var export_progress := 0.0
onready var gif_export_thread := Thread.new()
func _exit_tree():
if gif_export_thread.is_active():
gif_export_thread.wait_to_finish()
func process_frame() -> void:
var frame = Global.current_project.frames[frame_number - 1]
@ -127,18 +139,18 @@ func process_animation() -> void:
processed_images.append(image)
func export_processed_images(ignore_overwrites: bool, path_validation_alert_popup: AcceptDialog, file_exists_alert_popup: AcceptDialog, export_dialog: AcceptDialog ) -> bool:
func export_processed_images(ignore_overwrites: bool, export_dialog: AcceptDialog ) -> bool:
# Stop export if directory path or file name are not valid
var dir = Directory.new()
if not dir.dir_exists(directory_path) or not file_name.is_valid_filename():
path_validation_alert_popup.popup_centered()
export_dialog.open_path_validation_alert_popup()
return false
# Check export paths
var export_paths = []
for i in range(processed_images.size()):
stop_export = false
var multiple_files := true if (current_tab == ExportTab.ANIMATION && animation_type == AnimationType.MULTIPLE_FILES) else false
var multiple_files := true if (current_tab == ExportTab.ANIMATION and animation_type == AnimationType.MULTIPLE_FILES) else false
var export_path = create_export_path(multiple_files, i + 1)
# If user want to create new directory for each animation tag then check if directories exist and create them if not
if multiple_files and new_dir_for_each_frame_tag:
@ -152,8 +164,7 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu
# Ask user if he want's to overwrite the file
if not was_exported or (was_exported and not ignore_overwrites):
# Overwrite existing file?
file_exists_alert_popup.dialog_text = file_exists_alert % export_path
file_exists_alert_popup.popup_centered()
export_dialog.open_file_exists_alert_popup(file_exists_alert % export_path)
# Stops the function until the user decides if he want's to overwrite
yield(export_dialog, "resume_export_function")
if stop_export:
@ -161,29 +172,16 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu
return
export_paths.append(export_path)
# Only get one export path if single file animated image is exported
if current_tab == ExportTab.ANIMATION && animation_type == AnimationType.ANIMATED:
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
break
# Scale images that are to export
scale_processed_images()
if current_tab == ExportTab.ANIMATION && animation_type == AnimationType.ANIMATED:
var frame_delay_in_ms = Global.animation_timer.wait_time * 100
$GifExporter.begin_export(export_paths[0], processed_images[0].get_width(), processed_images[0].get_height(), frame_delay_in_ms, 0)
match direction:
AnimationDirection.FORWARD:
for i in range(processed_images.size()):
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
AnimationDirection.BACKWARDS:
for i in range(processed_images.size() - 1, -1, -1):
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
AnimationDirection.PING_PONG:
for i in range(0, processed_images.size()):
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
for i in range(processed_images.size() - 2, 0, -1):
$GifExporter.write_frame(processed_images[i], background_color, frame_delay_in_ms)
$GifExporter.end_export()
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
if gif_export_thread.is_active():
gif_export_thread.wait_to_finish()
gif_export_thread.start(self, "export_gif", {"export_dialog": export_dialog, "export_paths": export_paths})
else:
for i in range(processed_images.size()):
if OS.get_name() == "HTML5":
@ -197,10 +195,54 @@ func export_processed_images(ignore_overwrites: bool, path_validation_alert_popu
was_exported = true
store_export_settings()
Global.file_menu.get_popup().set_item_text(5, tr("Export") + " %s" % (file_name + file_format_string(file_format)))
# Only show when not exporting gif - gif export finishes in thread
if not (current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED):
Global.notification_label("File(s) exported")
return true
func export_gif(args: Dictionary) -> void:
# Export progress popup
export_progress_fraction = 100 / processed_images.size() # one fraction per each frame, one fraction for write to disk
export_progress = 0.0
args["export_dialog"].set_export_progress_bar(export_progress)
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())
match direction:
AnimationDirection.FORWARD:
for i in range(processed_images.size()):
write_frame_to_gif(processed_images[i], Global.animation_timer.wait_time, exporter, args["export_dialog"])
AnimationDirection.BACKWARDS:
for i in range(processed_images.size() - 1, -1, -1):
write_frame_to_gif(processed_images[i], Global.animation_timer.wait_time, exporter, args["export_dialog"])
AnimationDirection.PING_PONG:
export_progress_fraction = 100 / (processed_images.size() * 2)
for i in range(0, processed_images.size()):
write_frame_to_gif(processed_images[i], Global.animation_timer.wait_time, exporter, args["export_dialog"])
for i in range(processed_images.size() - 2, 0, -1):
write_frame_to_gif(processed_images[i], Global.animation_timer.wait_time, exporter, args["export_dialog"])
var file: File = File.new()
file.open(args["export_paths"][0], File.WRITE)
file.store_buffer(exporter.export_file_data())
file.close()
args["export_dialog"].toggle_export_progress_popup(false)
Global.notification_label("File(s) exported")
func write_frame_to_gif(image: Image, wait_time: float, exporter: Node, export_dialog: Node) -> void:
exporter.write_frame(image, wait_time, quantization)
increase_export_progress(export_dialog)
func increase_export_progress(export_dialog: Node) -> void:
export_progress += export_progress_fraction
export_dialog.set_export_progress_bar(export_progress)
func scale_processed_images() -> void:
for processed_image in processed_images:
if resize != 100:
@ -288,7 +330,6 @@ func store_export_settings() -> void:
exported_orientation = orientation
exported_lines_count = lines_count
exported_animation_type = animation_type
exported_background_color = background_color
exported_direction = direction
exported_resize = resize
exported_interpolation = interpolation
@ -305,7 +346,6 @@ func restore_previous_export_settings() -> void:
orientation = exported_orientation
lines_count = exported_lines_count
animation_type = exported_animation_type
background_color = exported_background_color
direction = exported_direction
resize = exported_resize
interpolation = exported_interpolation

View file

@ -11,8 +11,12 @@ onready var popups = $Popups
onready var file_exists_alert_popup = $Popups/FileExistsAlert
onready var path_validation_alert_popup = $Popups/PathValidationAlert
onready var path_dialog_popup = $Popups/PathDialog
onready var export_progress_popup = $Popups/ExportProgressBar
onready var export_progress_bar = $Popups/ExportProgressBar/MarginContainer/ProgressBar
onready var animation_options_multiple_animations_directories = $VBoxContainer/AnimationOptions/MultipleAnimationsDirectories
onready var previews = $VBoxContainer/PreviewScroll/Previews
onready var frame_timer = $FrameTimer
onready var frame_options = $VBoxContainer/FrameOptions
onready var frame_options_frame_number = $VBoxContainer/FrameOptions/FrameNumber/FrameNumber
@ -26,10 +30,8 @@ onready var spritesheet_options_lines_count_label = $VBoxContainer/SpritesheetOp
onready var animation_options = $VBoxContainer/AnimationOptions
onready var animation_options_animation_type = $VBoxContainer/AnimationOptions/AnimationType
onready var animation_options_animation_options = $VBoxContainer/AnimationOptions/AnimatedOptions
onready var animation_options_background_color = $VBoxContainer/AnimationOptions/AnimatedOptions/BackgroundColor
onready var animation_options_direction = $VBoxContainer/AnimationOptions/AnimatedOptions/Direction
onready var frame_timer = $FrameTimer
onready var options_resize = $VBoxContainer/Options/Resize
onready var options_interpolation = $VBoxContainer/Options/Interpolation
@ -38,8 +40,6 @@ onready var path_line_edit = $VBoxContainer/Path/PathLineEdit
onready var file_line_edit = $VBoxContainer/File/FileLineEdit
onready var file_file_format = $VBoxContainer/File/FileFormat
onready var animation_options_multiple_animations_directories = $VBoxContainer/AnimationOptions/MultipleAnimationsDirectories
func _ready() -> void:
tabs.add_tab("Frame")
@ -52,10 +52,8 @@ func _ready() -> void:
add_button("Cancel", false, "cancel")
file_exists_alert_popup.add_button("Cancel Export", false, "cancel")
# Disable GIF export for unsupported platforms
if not $GifExporter.is_platform_supported():
animation_options_animation_type.selected = Export.AnimationType.MULTIPLE_FILES
animation_options_animation_type.disabled = true
# Remove close button from export progress bar
export_progress_popup.get_close_button().hide()
func show_tab() -> void:
@ -95,7 +93,6 @@ func show_tab() -> void:
set_file_format_selector()
Export.process_animation()
animation_options_animation_type.selected = Export.animation_type
animation_options_background_color.color = Export.background_color
animation_options_direction.selected = Export.direction
animation_options.show()
set_preview()
@ -111,7 +108,7 @@ func external_export() -> void:
Export.process_spritesheet()
Export.ExportTab.ANIMATION:
Export.process_animation()
if Export.export_processed_images(true, path_validation_alert_popup, file_exists_alert_popup, self):
if Export.export_processed_images(true, self):
hide()
@ -216,6 +213,26 @@ func create_frame_tag_list() -> void:
spritesheet_options_frames.add_item(item.name)
func open_path_validation_alert_popup() -> void:
path_validation_alert_popup.popup_centered()
func open_file_exists_alert_popup(dialog_text: String) -> void:
file_exists_alert_popup.dialog_text = dialog_text
file_exists_alert_popup.popup_centered()
func toggle_export_progress_popup(open: bool) -> void:
if open:
export_progress_popup.popup_centered()
else:
export_progress_popup.hide()
func set_export_progress_bar(value: float) -> void:
export_progress_bar.value = value
func _on_ExportDialog_about_to_show() -> void:
# If export already occured - fill the dialog with previous export settings
if Export.was_exported:
@ -278,10 +295,6 @@ func _on_AnimationType_item_selected(id : int) -> void:
set_preview()
func _on_BackgroundColor_color_changed(color : Color) -> void:
Export.background_color = color
func _on_Direction_item_selected(id : int) -> void:
Export.direction = id
match id:
@ -303,7 +316,7 @@ func _on_Interpolation_item_selected(id: int) -> void:
func _on_ExportDialog_confirmed() -> void:
if Export.export_processed_images(false, path_validation_alert_popup, file_exists_alert_popup, self):
if Export.export_processed_images(false, self):
hide()

View file

@ -1,7 +1,6 @@
[gd_scene load_steps=3 format=2]
[gd_scene load_steps=2 format=2]
[ext_resource path="res://src/UI/Dialogs/ExportDialog.gd" type="Script" id=1]
[ext_resource path="res://addons/godot-gifexporter/src/GifExporter.gd" type="Script" id=2]
[node name="ExportDialog" type="AcceptDialog"]
margin_right = 532.0
@ -183,31 +182,14 @@ margin_right = 516.0
margin_bottom = 52.0
rect_min_size = Vector2( 0, 24 )
[node name="BackgroundColorLabel" type="Label" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
margin_top = 5.0
margin_right = 78.0
margin_bottom = 19.0
text = "Background:"
valign = 1
[node name="BackgroundColor" type="ColorPickerButton" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
margin_left = 82.0
margin_right = 263.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 7
color = Color( 1, 1, 1, 1 )
edit_alpha = false
[node name="DirectionLabel" type="Label" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
margin_left = 267.0
margin_top = 5.0
margin_right = 330.0
margin_right = 63.0
margin_bottom = 19.0
text = "Direction:"
[node name="Direction" type="OptionButton" parent="VBoxContainer/AnimationOptions/AnimatedOptions"]
margin_left = 334.0
margin_left = 67.0
margin_right = 516.0
margin_bottom = 24.0
rect_min_size = Vector2( 100, 0 )
@ -365,10 +347,10 @@ __meta__ = {
}
[node name="FileExistsAlert" type="AcceptDialog" parent="Popups"]
margin_left = 8.0
margin_top = 180.0
margin_right = 448.0
margin_bottom = 280.0
margin_left = 10.5227
margin_top = 176.636
margin_right = 450.523
margin_bottom = 276.636
size_flags_horizontal = 0
size_flags_vertical = 0
window_title = "Alarm!"
@ -378,16 +360,41 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="ExportProgressBar" type="WindowDialog" parent="Popups"]
margin_left = 63.0
margin_top = 215.0
margin_right = 402.0
margin_bottom = 256.0
popup_exclusive = true
window_title = "Exporting in progress..."
__meta__ = {
"_edit_group_": true,
"_edit_use_anchors_": false
}
[node name="MarginContainer" type="MarginContainer" parent="Popups/ExportProgressBar"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 5.0
margin_top = 5.0
margin_right = -5.0
margin_bottom = -5.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ProgressBar" type="ProgressBar" parent="Popups/ExportProgressBar/MarginContainer"]
margin_right = 329.0
margin_bottom = 14.0
size_flags_horizontal = 3
__meta__ = {
"_edit_use_anchors_": false
}
[node name="FrameTimer" type="Timer" parent="."]
__meta__ = {
"_editor_description_": "Timer to advance animation frames in animation preview."
}
[node name="GifExporter" type="Node" parent="."]
script = ExtResource( 2 )
__meta__ = {
"_editor_description_": ""
}
[connection signal="about_to_show" from="." to="." method="_on_ExportDialog_about_to_show"]
[connection signal="confirmed" from="." to="." method="_on_ExportDialog_confirmed"]
[connection signal="custom_action" from="." to="." method="_on_ExportDialog_custom_action"]
@ -399,7 +406,6 @@ __meta__ = {
[connection signal="value_changed" from="VBoxContainer/SpritesheetOptions/Orientation/LinesCount" to="." method="_on_LinesCount_value_changed"]
[connection signal="item_selected" from="VBoxContainer/AnimationOptions/AnimationType" to="." method="_on_AnimationType_item_selected"]
[connection signal="toggled" from="VBoxContainer/AnimationOptions/MultipleAnimationsDirectories" to="." method="_on_MultipleAnimationsDirectories_toggled"]
[connection signal="color_changed" from="VBoxContainer/AnimationOptions/AnimatedOptions/BackgroundColor" to="." method="_on_BackgroundColor_color_changed"]
[connection signal="item_selected" from="VBoxContainer/AnimationOptions/AnimatedOptions/Direction" to="." method="_on_Direction_item_selected"]
[connection signal="value_changed" from="VBoxContainer/Options/Resize" to="." method="_on_Resize_value_changed"]
[connection signal="item_selected" from="VBoxContainer/Options/Interpolation" to="." method="_on_Interpolation_item_selected"]