175 lines
5.8 KiB
GDScript3
175 lines
5.8 KiB
GDScript3
|
|
@tool
|
||
|
|
extends RefCounted
|
||
|
|
|
||
|
|
class_name ImageMagickConverter
|
||
|
|
|
||
|
|
static var _log: TwitchLogger = TwitchLogger.new("ImageMagickConverter")
|
||
|
|
|
||
|
|
## Current conversion in progress (key: path | value: mutex)
|
||
|
|
var converting: Dictionary = {}
|
||
|
|
var fallback_texture: Texture2D = preload("res://addons/twitcher/assets/fallback_texture.tres")
|
||
|
|
## Path to the imagemagic executable for example 'magick' when its in windows path
|
||
|
|
var imagemagic_path: String
|
||
|
|
|
||
|
|
static var delete_mutex: Mutex = Mutex.new()
|
||
|
|
static var folder_to_delete: Array[String] = []
|
||
|
|
|
||
|
|
## After that amount of files it starts to delete them
|
||
|
|
const DELETE_COUNT = 10
|
||
|
|
|
||
|
|
## Converts a packed byte array to a SpirteFrames and writes it out to the destination path
|
||
|
|
##
|
||
|
|
## The byte array must represent an animated gif, webp, or any imagemagick supported format
|
||
|
|
## it dumps it into a binary resource consisting of PNG frames.
|
||
|
|
##
|
||
|
|
## The resource is automatically added to the ResourceLoader cache as the input path value
|
||
|
|
func dump_and_convert(path: String, buffer_in: PackedByteArray = [], output = "%s.res" % path, parallel = false) -> SpriteFrames:
|
||
|
|
var thread: Thread = Thread.new()
|
||
|
|
var buffer: PackedByteArray = buffer_in.duplicate()
|
||
|
|
var mutex: Mutex
|
||
|
|
if parallel:
|
||
|
|
mutex = converting.get(path, Mutex.new())
|
||
|
|
converting[path] = mutex
|
||
|
|
|
||
|
|
var err = thread.start(_do_work.bind(path, buffer, output, mutex))
|
||
|
|
assert(err == OK, "could not start thread")
|
||
|
|
|
||
|
|
# don't block the main thread while loading
|
||
|
|
while not thread.is_started() or thread.is_alive():
|
||
|
|
await Engine.get_main_loop().process_frame
|
||
|
|
|
||
|
|
var tex: SpriteFrames = thread.wait_to_finish()
|
||
|
|
if parallel:
|
||
|
|
mutex.unlock()
|
||
|
|
converting.erase(path)
|
||
|
|
|
||
|
|
if not output.is_empty():
|
||
|
|
_save_converted_file(tex, output)
|
||
|
|
return tex
|
||
|
|
|
||
|
|
|
||
|
|
func _do_work(path: String, buffer: PackedByteArray, output: String, mutex: Mutex) -> SpriteFrames:
|
||
|
|
if mutex != null:
|
||
|
|
mutex.lock()
|
||
|
|
# load from cache if another thread already completed converting this same resource
|
||
|
|
if not output.is_empty() and ResourceLoader.has_cached(output):
|
||
|
|
return ResourceLoader.load(output)
|
||
|
|
|
||
|
|
# dump the buffer
|
||
|
|
if FileAccess.file_exists(path):
|
||
|
|
_log.i("File found at %s, loading it instead of using the buffer." % path)
|
||
|
|
buffer = FileAccess.get_file_as_bytes(path)
|
||
|
|
else:
|
||
|
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||
|
|
var f = FileAccess.open(path, FileAccess.WRITE)
|
||
|
|
if f == null:
|
||
|
|
_log.e("Can't open file %s cause of %s" %[ path, FileAccess.get_open_error()])
|
||
|
|
f.store_buffer(buffer)
|
||
|
|
f.close()
|
||
|
|
|
||
|
|
var frame_delays: Array[int] = _get_frame_delay(path)
|
||
|
|
var folder_path: String = _create_temp_filename()
|
||
|
|
if not _extract_images(path, folder_path):
|
||
|
|
return _create_fallback_texture()
|
||
|
|
var sprite_frames: SpriteFrames = _build_frames(folder_path, frame_delays)
|
||
|
|
if not output.is_empty():
|
||
|
|
sprite_frames.take_over_path(output)
|
||
|
|
|
||
|
|
# delete the temp directory
|
||
|
|
delete_mutex.lock()
|
||
|
|
folder_to_delete.append(folder_path)
|
||
|
|
delete_mutex.unlock()
|
||
|
|
|
||
|
|
_cleanup()
|
||
|
|
return sprite_frames
|
||
|
|
|
||
|
|
|
||
|
|
## Saves the texture to the output path
|
||
|
|
func _save_converted_file(tex: SpriteFrames, output: String):
|
||
|
|
if not output.is_empty() and tex:
|
||
|
|
ResourceSaver.save(tex, output, ResourceSaver.SaverFlags.FLAG_COMPRESS)
|
||
|
|
tex.take_over_path(output)
|
||
|
|
|
||
|
|
|
||
|
|
func _create_unique_key(length: int = 8) -> String:
|
||
|
|
var uniq = ""
|
||
|
|
for i in range(length):
|
||
|
|
uniq += "%d" % [randi() % 10]
|
||
|
|
return uniq
|
||
|
|
|
||
|
|
|
||
|
|
## Creates a folder to store the extracted images (needs the / at the end!)
|
||
|
|
func _create_temp_filename() -> String:
|
||
|
|
var folder_path: String = ""
|
||
|
|
var uniq = _create_unique_key()
|
||
|
|
if Engine.is_editor_hint():
|
||
|
|
folder_path = "res://.godot/magick_tmp/%s_%d/" % [uniq, Time.get_unix_time_from_system()]
|
||
|
|
else:
|
||
|
|
folder_path = "user://.magick_tmp/%s_%d/" % [uniq, Time.get_unix_time_from_system()]
|
||
|
|
|
||
|
|
_log.i("Create temp folder")
|
||
|
|
DirAccess.make_dir_recursive_absolute(folder_path)
|
||
|
|
return folder_path
|
||
|
|
|
||
|
|
|
||
|
|
## Extracts all delays from the file in seconds
|
||
|
|
func _get_frame_delay(file: String) -> Array[int]:
|
||
|
|
var out = []
|
||
|
|
var glob_path = ProjectSettings.globalize_path(file)
|
||
|
|
OS.execute(imagemagic_path, [ glob_path, "-format", "%T\\n", "info:" ], out)
|
||
|
|
var frame_delays: Array[int] = []
|
||
|
|
for delay in out[0].split("\n"):
|
||
|
|
# convert x100 to x1000(ms)
|
||
|
|
frame_delays.append(delay.to_int() * 10)
|
||
|
|
return frame_delays
|
||
|
|
|
||
|
|
|
||
|
|
## Extracts all images from the file and saves them to folder path
|
||
|
|
func _extract_images(file: String, target_folder: String) -> bool:
|
||
|
|
var out = []
|
||
|
|
var glob_file_path = ProjectSettings.globalize_path(file)
|
||
|
|
var glob_extracted_file_path = ProjectSettings.globalize_path(target_folder + "%04d.png")
|
||
|
|
var code = OS.execute(imagemagic_path, [ "convert", "-coalesce", glob_file_path, glob_extracted_file_path ], out, true)
|
||
|
|
if code != 0:
|
||
|
|
_log.e("unable to convert: %s" % "\n".join(out))
|
||
|
|
return false
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _create_fallback_texture():
|
||
|
|
var sprite_frames = SpriteFrames.new()
|
||
|
|
sprite_frames.add_frame(&"default", fallback_texture)
|
||
|
|
return sprite_frames
|
||
|
|
|
||
|
|
|
||
|
|
func _build_frames(folder_path: String, frame_delays: Array[int]):
|
||
|
|
_log.i("Build Frames")
|
||
|
|
var frames = DirAccess.get_files_at(folder_path)
|
||
|
|
if len(frames) == 0:
|
||
|
|
return _create_fallback_texture()
|
||
|
|
|
||
|
|
var sprite_frames: SpriteFrames = SpriteFrames.new()
|
||
|
|
for filepath in frames:
|
||
|
|
var idx = filepath.substr(0, filepath.rfind(".")).to_int()
|
||
|
|
var delay = frame_delays[idx] / 1000.0
|
||
|
|
var image = Image.new()
|
||
|
|
var error = image.load(folder_path + filepath)
|
||
|
|
if error != OK:
|
||
|
|
return _create_fallback_texture()
|
||
|
|
|
||
|
|
var frame = ImageTexture.create_from_image(image)
|
||
|
|
sprite_frames.add_frame(&"default", frame, delay)
|
||
|
|
sprite_frames.set_animation_speed(&"default", 1)
|
||
|
|
return sprite_frames
|
||
|
|
|
||
|
|
|
||
|
|
## Cleans after DELETE_COUNT amount of folder entries
|
||
|
|
func _cleanup(force: bool = false):
|
||
|
|
if folder_to_delete.size() % DELETE_COUNT == 0 || force:
|
||
|
|
delete_mutex.lock()
|
||
|
|
for folder in folder_to_delete:
|
||
|
|
var glob_folder = ProjectSettings.globalize_path(folder)
|
||
|
|
OS.move_to_trash(glob_folder)
|
||
|
|
folder_to_delete.clear()
|
||
|
|
delete_mutex.unlock()
|