Initial Commit

Initial commit of Code Base.
This commit is contained in:
Mario Steele 2025-06-12 14:31:14 -05:00
parent 293b1213e1
commit c11a4ebbc2
653 changed files with 36893 additions and 1 deletions

View file

@ -0,0 +1,174 @@
@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()