From 2039019cf2d2082655c1c89bedcc9e1727c69102 Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Thu, 14 Nov 2024 17:13:41 -0800 Subject: [PATCH] Adds support for reading OTIOZ (#85) * Reads content.otio from OTIOZ files * Added minizip-ng submodule Signed-off-by: Joshua Minor Co-authored-by: Thomas Wilshaw Co-authored-by: Darby Johnston --- .gitmodules | 3 + CMakeLists.txt | 19 ++++++ app.cpp | 176 +++++++++++++++++++++++++++++++++++++++++------- libs/minizip-ng | 1 + 4 files changed, 173 insertions(+), 26 deletions(-) create mode 160000 libs/minizip-ng diff --git a/.gitmodules b/.gitmodules index 1c493e5..a425b63 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "libs/ImGuiColorTextEdit"] path = libs/ImGuiColorTextEdit url = https://github.com/jminor/ImGuiColorTextEdit.git +[submodule "libs/minizip-ng"] + path = libs/minizip-ng + url = https://github.com/zlib-ng/minizip-ng.git diff --git a/CMakeLists.txt b/CMakeLists.txt index efcebde..93382c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,24 @@ if(NOT EMSCRIPTEN AND NOT WIN32) add_subdirectory("libs/glfw") endif() +# minizip-ng +set(BUILD_SHARED_LIBS OFF) +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +set(MZ_FETCH_LIBS ON) +set(MZ_ZLIB ON) +# OTIOZ doesn't need any of these +set(MZ_BZIP2 OFF) +set(MZ_LZMA OFF) +set(MZ_ZSTD OFF) +set(MZ_LIBCOMP OFF) +set(MZ_PKCRYPT OFF) +set(MZ_WZAES OFF) +set(MZ_OPENSSL OFF) +set(MZ_BCRYPT OFF) +set(MZ_LIBBSD OFF) +set(MZ_ICONV OFF) +add_subdirectory("libs/minizip-ng") + if(NOT EMSCRIPTEN) add_custom_command( OUTPUT "${PROJECT_SOURCE_DIR}/fonts/embedded_font.inc" @@ -81,6 +99,7 @@ target_compile_definitions(raven target_link_libraries(raven PUBLIC OTIO::opentimelineio IMGUI + MINIZIP::minizip ) if (APPLE) diff --git a/app.cpp b/app.cpp index e26e4ee..af0ca6b 100644 --- a/app.cpp +++ b/app.cpp @@ -1,6 +1,8 @@ // Raven NLE +#include #include +#include #include #include @@ -15,10 +17,15 @@ #include "nfd.h" #endif +#include "mz.h" +#include "mz_zip.h" +#include "mz_strm.h" +#include "mz_zip_rw.h" + #include "fonts/embedded_font.inc" #include -#include +#include void DrawMenu(); void DrawToolbar(ImVec2 buttonSize); @@ -271,9 +278,7 @@ void LoadString(std::string json) { elapsed_seconds); } -void LoadFile(std::string path) { - auto start = std::chrono::high_resolution_clock::now(); - +otio::Timeline* LoadOTIOFile(std::string path) { otio::ErrorStatus error_status; auto timeline = dynamic_cast( otio::Timeline::from_json_file(path, &error_status)); @@ -282,9 +287,141 @@ void LoadFile(std::string path) { "Error loading \"%s\": %s", path.c_str(), otio_error_string(error_status).c_str()); + return nullptr; + } + return timeline; +} + +otio::Timeline* LoadOTIOZFile(std::string path) { + otio::Timeline* timeline = nullptr; + + void *zip_reader = mz_zip_reader_create(); + + auto status = mz_zip_reader_open_file(zip_reader, path.c_str()); + if (status != MZ_OK) { + Message( + "Error opening \"%s\": %d", + path.c_str(), + status); + } else { + status = mz_zip_reader_locate_entry(zip_reader, "content.otio", 1); + if (status != MZ_OK) { + Message( + "Invalid OTIOZ: \"%s\": \"content.otio\" not found in archive.", + path.c_str()); + } else { + mz_zip_file *file_info = NULL; + status = mz_zip_reader_entry_get_info(zip_reader, &file_info); + if (status != MZ_OK) { + Message( + "Invalid OTIOZ: \"%s\": Error getting entry info: %d", + path.c_str(), + status); + } else { + status = mz_zip_reader_entry_open(zip_reader); + if (status != MZ_OK) { + Message( + "Invalid OTIOZ: \"%s\": Unable to open entry: %d", + path.c_str(), + status); + } else { + char* buf = (char*)malloc(file_info->uncompressed_size + 1); + char* buf_cursor = buf; + int64_t bytes_remaining = file_info->uncompressed_size; + int32_t bytes_read = 0; + while (bytes_remaining > 0) { + int32_t chunk_size = bytes_remaining < INT32_MAX ? bytes_remaining : INT32_MAX; + bytes_read = mz_zip_reader_entry_read(zip_reader, buf_cursor, chunk_size); + if (bytes_read > 0) { + bytes_remaining -= bytes_read; + buf_cursor += bytes_read; + } else { + break; + } + } + if (bytes_remaining != 0) { + Message( + "Invalid OTIOZ: \"%s\": Error reading entry: %ld", + path.c_str(), + bytes_remaining); + } else { + // Add a null terminator + buf[file_info->uncompressed_size] = '\0'; + std::string json(buf); + timeline = dynamic_cast( + otio::Timeline::from_json_string(json)); + } + free(buf); + mz_zip_reader_entry_close(zip_reader); + } + } + } + + mz_zip_reader_close(zip_reader); + } + + mz_zip_reader_delete(&zip_reader); + + return timeline; +} + +std::string FileExtension(std::string path) { + return path.substr(path.find_last_of(".") + 1); +} + +std::string LowerCase(std::string str) { + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); }); // 🙄 + return str; +} + +// Validate that a file has the .otio or .otioz extension +bool SupportedFileType(const std::string& filepath) { + size_t last_dot = filepath.find_last_of('.'); + + // If no dot is found, it's not a valid file + if (last_dot == std::string::npos) { + return false; + } + + // Get and check the extension + std::string extension = filepath.substr(last_dot + 1); + return extension == "otio" || extension == "otioz"; +} + +void LoadFile(std::string path) { + otio::Timeline* timeline = nullptr; + + auto start = std::chrono::high_resolution_clock::now(); + + if (!std::filesystem::exists(path)) { + Message( + "File not found \"%s\"", + path.c_str()); return; } + if (!SupportedFileType(path)) { + Message( + "Unsuported file type \"%s\"", + path.c_str()); + return; + } + + auto ext = LowerCase(FileExtension(path)); + if (ext == "otio") { + timeline = LoadOTIOFile(path); + } else if (ext == "otioz") { + timeline = LoadOTIOZFile(path); + } else { + Message( + "Unsupported file type \"%s\"", + ext.c_str()); + return; + } + + if (!timeline) + return; + LoadTimeline(timeline); appState.file_path = path; @@ -294,7 +431,7 @@ void LoadFile(std::string path) { double elapsed_seconds = elapsed.count(); Message( "Loaded \"%s\" in %.3f seconds", - timeline->name().c_str(), + path.c_str(), elapsed_seconds); } @@ -335,30 +472,18 @@ void MainInit(int argc, char** argv, int initial_width, int initial_height) { LoadFonts(); + // Load an empty timeline if no file is provided + // or if loading the file fails for some reason. + auto tl = new otio::Timeline(); + LoadTimeline(tl); + if (argc > 1) { LoadFile(argv[1]); - } else { - auto tl = new otio::Timeline(); - LoadTimeline(tl); } } void MainCleanup() { } -// Validate that a file has the .otio extension -bool is_valid_file(const std::string& filepath) { - size_t last_dot = filepath.find_last_of('.'); - - // If no dot is found, it's not a valid file - if (last_dot == std::string::npos) { - return false; - } - - // Get and check the extension - std::string extension = filepath.substr(last_dot + 1); - return extension == "otio"; -} - // Accept and open a file path void FileDropCallback(int count, const char** filepaths) { if (count > 1){ @@ -372,14 +497,13 @@ void FileDropCallback(int count, const char** filepaths) { std::string file_path = filepaths[0]; - if (!is_valid_file(file_path)){ - Message("Invalid file: %s", file_path.c_str()); + if (!SupportedFileType(file_path)){ + Message("Unsupported file type: %s", file_path.c_str()); return; } // Loading is done in DrawDroppedFilesPrompt() prompt_dropped_file = file_path; - } // Make a button using the fancy icon font @@ -863,7 +987,7 @@ void DrawDroppedFilesPrompt() { // Modal window for confirmation if (ImGui::BeginPopupModal("Open File?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Open file \n%s?", prompt_dropped_file.c_str()); - + if (ImGui::Button("Yes")) { LoadFile(prompt_dropped_file); prompt_dropped_file = ""; diff --git a/libs/minizip-ng b/libs/minizip-ng new file mode 160000 index 0000000..fe5fedc --- /dev/null +++ b/libs/minizip-ng @@ -0,0 +1 @@ +Subproject commit fe5fedc365f7824ada0cf9a84fb79b30d5fc97a8