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