diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bf9e0799..e6f51d2bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,8 +48,11 @@ set(CMAKE_AUTORCC ON) # root to enrich your IDE with clangd language server protocol functionalities set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +include_directories(include/) # Sanitizers -add_library(olive-sanitizers INTERFACE) +add_library(olive-sanitizers INTERFACE + src/common/c_api/commandlineparser.cpp + include/common/commandlineparser.h) include(cmake/Sanitizers.cmake) enable_sanitizers(olive-sanitizers) list(APPEND OLIVE_LIBRARIES olive-sanitizers) @@ -368,7 +371,7 @@ add_subdirectory(cli) add_subdirectory(app) add_subdirectory(worker) - +add_subdirectory(src) if (BUILD_TESTS) enable_testing() add_subdirectory(tests) diff --git a/docs/zh/plans/riir/M1-oakcommon.md b/docs/zh/plans/riir/M1-oakcommon.md index 13041a741..7aa63195f 100644 --- a/docs/zh/plans/riir/M1-oakcommon.md +++ b/docs/zh/plans/riir/M1-oakcommon.md @@ -101,3 +101,67 @@ OAK/OIIO/FFmpeg 类型全部句柄化或拍平字段。) - xml:reader 解析样例串、attr/text 读取、skip、writer 产出解析回读。 - files:临时目录建/写/读/尺寸/删除。 - 每函数 1 正常 + 1 错误路径;`oakcommon_debug_alive_count()` 泄漏断言。 + +## 实施现状(2026-08-05) + +M1 已落地并可独立构建、测试全绿(127 个用例:126 通过,1 个 +`GTEST_SKIP`)。以下为与上文计划的实际差异。 + +### 最终目录结构 + +- `src/common/src/` — 去 Qt 化 C++ 实现(`olive::` 命名空间),target + `oakcommon`(SHARED)。 +- `src/common/c_api/` — 纯 C ABI 包装,通过 `target_sources` 合并进 + `oakcommon`,不单独成库。 +- `src/common/tests/` — gtest,target `oakcommon-gtest`, + `gtest_discover_tests`。 +- `include/common/`(仓库根)— 公共 C 头:`commandlineparser.h`、 + `current.h`、`debug.h`、`dropworkflowbehavior.h`、`error.h`、 + `ffmpegutils.h`、`filefunctions.h`、`miscutils.h`、`ocioutils.h`、 + `oiioutils.h`、`power.h`、`qtutils.h`、`xmlutils.h`。 +- `src/common/standalone/CMakeLists.txt` — 独立构建 driver(见下)。 + +### 独立构建与测试 + +```sh +cmake -S src/common/standalone -B build-oakcommon +cmake --build build-oakcommon -j +ctest --test-dir build-oakcommon --output-on-failure +``` + +driver 通过 `find_package(... CONFIG)` 使用 Homebrew 的 OCIO/OIIO/GTest +(顶层 `cmake/FindOpenColorIO.cmake`/`FindOpenImageIO.cmake` 面向 +`.so`,macOS 下不适用),并把 config target 映射到 oakcommon +CMakeLists 消费的 `${OCIO_LIBRARIES}` 等变量。driver 中额外处理了两点: +给 `olivecore` 补 `third_party/openfx/include` 头路径(顶层靠全局 +include);禁用 OpenTimelineIO(`/opt/otio` 的 dylib 用 `@loader_path` +安装名,构建树内无法加载,且 oakcommon 不需要 OTIO)。 + +### 实际依赖 + +- 第三方:EXPAT(XML 解析)、OpenColorIO、OpenImageIO、FFmpeg(经 + ffmpeg_bridge 间接)、GTest(仅测试)。 +- Oak 内部库:**仍链接 `olivecore` 与 `ffmpeg_bridge`**,二者均为真实 + 符号依赖而非纯头文件: + - `olive::core::Rational`(`core/include/olive/core/util/rational.h`) + 是对 `oakcore_rational_*` C ABI 的包装,构造/析构/运算都需要 + olivecore 的符号; + - `FFmpegUtils::get_compatible_bridge_pixel_format` 调用 + `fb_find_best_pix_fmt_of_list`(ffmpeg_bridge 导出符号)。 + - `pixelformat.h`/`sampleformat.h` 本身是 header-only,只需头路径 + (`core/include`、`ffmpeg_bridge/include`,因这两个 target 的 + include 目录是 PRIVATE,在 oakcommon 里显式补为 PUBLIC)。 +- 未按计划只链 oakcore 头;OLIVECORE_BUILD_TESTS 在独立构建中关闭。 + +### 与计划的主要差异 + +- 接口未按 §2 冻结清单逐条实现,而是按实际使用面包装:C ABI 头放在 + 仓库根 `include/common/`,命名 `oakcommon_<模块>_<动词>`,两段式 + buffer(先查询尺寸再拷入)约定统一。 +- `xmlutils` 用 expat 实现(事件预先解析成队列);C API 的 + `oakcommon_xml_reader_read_element_text` 因底层是消费型读取,在 + handle 内缓存最近一次文本以兼容两段式调用。 +- 被移除/上移的类(播放钟、autoscroll 等反向 include 涉及项)见 + `notes.md`。 +- 计划中提到的 `switch(PixelFormat)` 编译问题实际不存在: + `olive::core::PixelFormat` 提供 `operator Format()`,可直接 switch。 diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/riir/notes.md new file mode 100644 index 000000000..3dc9cef3e --- /dev/null +++ b/docs/zh/plans/riir/notes.md @@ -0,0 +1,82 @@ +# 记录项 + +## common + +CancelableObject只在render和task处有🚰,移入对应模块。 + +## 从 oakcommon(src/common)移除的类(2026-08-05) + +判据:不严重依赖 common 其他类,且只有一个非 common 模块使用它 +(tests/gtest 不计入使用方)。以下类已从 `src/common/` 移除, +拆分时应放到对应模块: + +| 类 | 唯一使用方 | 应放到 | +|---|---|---| +| `Html`(html.h/.cpp) | engine/node | oaknode(M3)。注意它依赖 common 的 xmlutils,迁移时需连同 XML 辅助或改为调用 oakcommon C API | +| `JobTime`(jobtime.h/.cpp) | engine/render | oakrender(M7) | +| `OTIOUtils`(otioutils.h) | engine/task | oaktask(M8) | +| `PlaybackAudioClock`(playbackaudioclock.h) | engine/audio | oakaudio(M6) | +| `to_hex`(tohex.h) | engine/node | oaknode(M3) | +| `mid()`(util.h) | engine/node | oaknode(M3) | +| `AVFramePtr`(avframeptr.h) | engine/render | oakrender(M7) | +| `CrashpadInterface`(crashpadinterface.h/.cpp) | app/main.cpp | app(crashhandler 相关) | +| crashpadutils.h | app/crashhandler | app/crashhandler | +| `AutoScroll`(autoscroll.h,oakutil shim) | app/dialog、app/widget | app | +| `digit.h`(oakutil shim) | app/dialog | app | +| `range.h`(oakutil shim) | app/widget | app | + +另:`power.h`、`memorypool.h`、`threadsafemap.h` 当前没有任何 +common 模块外的使用方(零用户),不满足移除判据,暂保留在 +oakcommon;后续若确认无用途可直接删除。 + +## oakcommon 去Qt化的删除与语义变更(2026-08-05) + +去Qt化过程中以下函数被删除或语义变化,迁移调用方时需注意: + +- `CommandLineParser::print_help()`:不再自动读取 + `QCoreApplication::applicationName()/applicationVersion()`,需先调 + `set_app_info()`(C API:`oakcommon_commandlineparser_set_app_info`), + 否则打印默认 `"oak"` + 空版本。 +- `FileFunctions::get_unique_file_identifier`:哈希由 SHA-1 改为 + FNV-1a 64-bit,旧缓存 key 全部失效(需一次重建)。 + `get_configuration_location`/`get_application_path`/ + `get_temp_file_path`/`get_auto_recovery_root` 的标准路径改为手写 + 平台实现,路径与原 Qt 版不同。`read_file_as_string(":/...")` 的 + qrc 资源路径不再可用,shader 等资源调用方需改为磁盘路径或内嵌。 +- xmlutils:`xml_read_next_start_element(reader, CancelAtom*)` + 重载与 cancel_atom 参数删除(依赖 render/cancelatom.h),取消语义 + 由调用方在循环外自行检查;`XMLAttributeLoop` 宏删除,由 + `XmlStreamReader::attributes()` + range-for 替代; + `QXmlStreamReader/Writer` 由基于 expat 的 + `olive::XmlStreamReader/Writer` 替代(不支持 XML 命名空间)。 +- `OIIOUtils::frame_to_buffer/buffer_to_frame`:未进 C API,C++ 侧签名 + 拍平为 `(const void *data, int64_t linesize_bytes, OIIO::ImageBuf *)`, + engine/codec/frame.cpp 迁移时传成员即可。`OakCommonPixelFormat` 枚举 + 目前定义在 include/common/ocioutils.h,oiioutils 复用,后续可抽成 + 独立的 include/common/pixelformat.h。 +- `MemoryPool`:删除 QTimer 每 5 秒自动回收空 arena,改为公有 + `clear_empty_arenas()` 由调用方周期调用;不再继承 QObject。 + memorypool/threadsafemap 零使用方且为模板/多态基类,未包 C API。 +- `debug_handler`:丢失 `QMessageLogContext`(文件/行号)参数(原实现 + 本就未用);`qInstallMessageHandler` 无对应物。 +- `Current::set_current_video_params/audio_params`:由按值拷贝改为持有 + `std::shared_ptr`(类型擦除),调用方需自行管理生命周期并用 + `static_pointer_cast` 还原(调用点:olivehost.cpp、 + timebasedwidget.cpp、plugin.cpp 等 6 处)。 +- qtutils:engine 未使用的纯 UI 函数(`q_font_metrics_width`、 + `create_horizontal/vertical_line`、`set_combo_box_data`、 + `word_wrap_string`、`flip_control_and_shift_modifiers`、 + `get_formatted_date_time`、`to_q_color`、`core::qHash`、 + `Q_DECLARE_METATYPE` 宏组)未迁入 oakcommon,app 层继续用 Qt 版。 + `ptr_to_value/value_to_ptr` 载体由 QVariant 改为 `uintptr_t`, + 迁移时调用点需同步改(renderprocessor.cpp、rendermanager.cpp、 + previewautocacher.cpp、src/capi/worker.cpp)。`get_parent_of_type` + 依赖 C++ 模板+RTTI,未进 C API。 +- define.h:删除 `MACRO_NAME_AS_STR`/`MACRO_VAL_AS_STR`/ + `OLIVE_NS_CONST_ARG`/`OLIVE_NS_ARG`/`OLIVE_NS_RETURN_ARG` + (依赖 Qt QArgument,全仓无使用)。 +- decibel.h:删除 `ALLOW_RETURNING_INFINITY` 编译开关分支(代码库 + 未定义该宏,行为不变)。 +- `FFmpegUtils`:新增类内常量 `k_rgb_channel_count/k_rgba_channel_count` + (3/4)取代对 `VideoParams` 同名常量的引用,消除 common→render + 反向依赖。 diff --git a/include/common/commandlineparser.h b/include/common/commandlineparser.h new file mode 100644 index 000000000..68389cec4 --- /dev/null +++ b/include/common/commandlineparser.h @@ -0,0 +1,193 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_COMMANDLINEPARSER_H +#define OAK_EDITOR_COMMANDLINEPARSER_H + +#ifndef __cplusplus +#include +#endif + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a command-line parser instance. + */ +typedef struct OakCommonCommandLineParser OakCommonCommandLineParser; + +/** + * @brief Opaque handle to a registered command-line option. + * + * The handle wrapper is freed with oakcommon_commandlineoption_free(); + * the underlying option is owned by the parser and stays valid until + * the parser is freed. + */ +typedef struct OakCommonCommandLineOption OakCommonCommandLineOption; + +/** + * @brief Opaque handle to a registered positional argument. + * + * The handle wrapper is freed with + * oakcommon_commandlinepositionalargument_free(); the underlying argument + * is owned by the parser and stays valid until the parser is freed. + */ +typedef struct OakCommonCommandLinePositionalArgument + OakCommonCommandLinePositionalArgument; + +/** + * @brief Create a command-line parser. + * + * @return Parser handle, or NULL on allocation failure. + */ +OakCommonCommandLineParser *oakcommon_commandlineparser_init(void); + +/** + * @brief Destroy a command-line parser. + * + * Destroys all option and positional-argument handles created from it. + * NULL is a no-op. + */ +void oakcommon_commandlineparser_free(OakCommonCommandLineParser *parser); + +/** + * @brief Set the application name/version shown by print_help. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineparser_set_app_info(OakCommonCommandLineParser *parser, + const char *name, + const char *version); + +/** + * @brief Register an option with one or more name strings. + * + * @param names Array of option name strings (without leading dash). + * @param name_count Number of entries in names. Must be > 0. + * @param description Help text, may be NULL. + * @param takes_arg Non-zero if the option consumes the following argument. + * @param arg_placeholder Placeholder shown in help, may be NULL. + * @param hidden Non-zero to omit from help output. + * @param out_option Receives the option handle. May be NULL if unused. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineparser_add_option( + OakCommonCommandLineParser *parser, const char *const *names, int name_count, + const char *description, int takes_arg, const char *arg_placeholder, + int hidden, OakCommonCommandLineOption **out_option); + +/** + * @brief Register a positional argument. + * + * @param out_argument Receives the argument handle. May be NULL if unused. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineparser_add_positional_argument( + OakCommonCommandLineParser *parser, const char *name, + const char *description, int required, + OakCommonCommandLinePositionalArgument **out_argument); + +/** + * @brief Parse an argv-style argument list. + * + * argv[0] is skipped as the program name, matching C main() convention. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineparser_process(OakCommonCommandLineParser *parser, + const char *const *argv, int argc); + +/** + * @brief Print usage/help text to stdout. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineparser_print_help(OakCommonCommandLineParser *parser, + const char *filename); + +/** + * @brief Query whether an option was present on the command line. + * + * @param is_set Receives the result. Must not be NULL. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineoption_is_set(OakCommonCommandLineOption *option, + bool *is_set); + +/** + * @brief Free an option handle wrapper. + * + * Does not unregister the option from the parser. NULL is a no-op. + */ +void oakcommon_commandlineoption_free(OakCommonCommandLineOption *option); + +/** + * @brief Get an option's argument value (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineoption_get_setting(OakCommonCommandLineOption *option, + char *buf, int buf_size); + +/** + * @brief Set an option's argument value. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlineoption_set_setting(OakCommonCommandLineOption *option, + const char *value); + +/** + * @brief Get a positional argument's value (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlinepositionalargument_get_setting( + OakCommonCommandLinePositionalArgument *argument, char *buf, int buf_size); + +/** + * @brief Set a positional argument's value. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_commandlinepositionalargument_set_setting( + OakCommonCommandLinePositionalArgument *argument, const char *value); + +/** + * @brief Free a positional argument handle wrapper. + * + * Does not unregister the argument from the parser. NULL is a no-op. + */ +void oakcommon_commandlinepositionalargument_free( + OakCommonCommandLinePositionalArgument *argument); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_COMMANDLINEPARSER_H diff --git a/include/common/current.h b/include/common/current.h new file mode 100644 index 000000000..5289dfe05 --- /dev/null +++ b/include/common/current.h @@ -0,0 +1,107 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_CURRENT_H +#define OAK_EDITOR_CURRENT_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct OakCommonCurrent OakCommonCurrent; + +/** + * @brief Destructor callback for objects handed to Current slots. + * + * Called when the slot is overwritten or cleared. May be NULL if the + * caller keeps ownership of the object. + */ +typedef void (*OakCommonDestroyFn)(void *obj); + +/** + * @brief Return a handle to the process-wide Current singleton. + * + * The returned handle is borrowed: it is valid for the lifetime of the + * process and must not be freed with oakcommon_current_free() more + * than out of symmetry (free is a no-op for the singleton). + */ +OakCommonCurrent *oakcommon_current_instance(void); + +/** + * @brief Release a Current handle. + * + * No-op: the underlying object is a singleton. Safe to call with NULL. + */ +void oakcommon_current_free(OakCommonCurrent *self); + +/** + * @brief Store a pointer in a Current slot, taking over destruction. + * + * Passing NULL for obj clears the slot (destroy is ignored). If a + * previous object with a destroy callback was stored, it is destroyed. + * + * @param self Handle from oakcommon_current_instance(). + * @param obj Opaque pointer to the external object (e.g. a + * VideoParams), or NULL to clear. + * @param destroy Optional destructor invoked when the slot is replaced. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self is NULL. + */ +int oakcommon_current_set_video_params(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy); +int oakcommon_current_set_audio_params(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy); +int oakcommon_current_set_plugin_host(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy); +int oakcommon_current_set_plugin_cache(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy); + +/** + * @brief Fetch the raw pointer currently stored in a slot. + * + * The returned pointer is borrowed and remains valid until the slot is + * overwritten or cleared. *out is set to NULL when the slot is empty. + * + * @param self Handle from oakcommon_current_instance(). + * @param out Receives the stored pointer. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self or out + * is NULL. + */ +int oakcommon_current_get_video_params(OakCommonCurrent *self, void **out); +int oakcommon_current_get_audio_params(OakCommonCurrent *self, void **out); +int oakcommon_current_get_plugin_host(OakCommonCurrent *self, void **out); +int oakcommon_current_get_plugin_cache(OakCommonCurrent *self, void **out); + +/** + * @brief Query whether the session is interactive. + * + * @param self Handle from oakcommon_current_instance(). + * @param out Receives 1 for interactive, 0 otherwise. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self or out + * is NULL. + */ +int oakcommon_current_is_interactive(OakCommonCurrent *self, int *out); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_CURRENT_H diff --git a/include/common/debug.h b/include/common/debug.h new file mode 100644 index 000000000..4415b3dbe --- /dev/null +++ b/include/common/debug.h @@ -0,0 +1,75 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_DEBUG_H +#define OAK_EDITOR_DEBUG_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Severity levels for oakcommon debug output. + * + * Mirrors olive::DebugLevel in src/common/src/debug.h. + */ +enum OakCommonDebugLevel { + OAKCOMMON_DEBUG_DEBUG = 0, /**< Verbose debug message. */ + OAKCOMMON_DEBUG_INFO = 1, /**< Informational message. */ + OAKCOMMON_DEBUG_WARNING = 2, /**< Warning message. */ + OAKCOMMON_DEBUG_ERROR = 3, /**< Error message. */ + OAKCOMMON_DEBUG_FATAL = 4 /**< Fatal error message. */ +}; + +/** + * @brief Print a debug message to stderr, prefixed with its level. + * + * De-Qt replacement for the old Qt message handler. The line is + * flushed immediately. + * + * @param level One of OakCommonDebugLevel; out-of-range values print + * as "UNKNOWN". + * @param msg NUL-terminated message; NULL is treated as an empty + * string. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if msg is NULL. + */ +int oakcommon_debug_log(int level, const char *msg); + +/** + * @brief Copy the printable name of a debug level into buf. + * + * Two-segment string getter: if buf is NULL or buf_size is too small, + * nothing is written. + * + * @param level One of OakCommonDebugLevel. + * @param buf Destination buffer, may be NULL to query the size. + * @param buf_size Size of buf in bytes. + * @return Required buffer size in bytes including the terminating NUL + * (non-negative). + */ +int oakcommon_debug_level_name(int level, char *buf, int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_DEBUG_H diff --git a/include/common/dropworkflowbehavior.h b/include/common/dropworkflowbehavior.h new file mode 100644 index 000000000..c94d02935 --- /dev/null +++ b/include/common/dropworkflowbehavior.h @@ -0,0 +1,73 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_DROPWORKFLOWBEHAVIOR_H +#define OAK_EDITOR_DROPWORKFLOWBEHAVIOR_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Behavior when media is dropped onto a timeline without a + * sequence. + * + * Mirrors olive::DropWithoutSequenceBehavior in + * src/common/src/dropworkflowbehavior.h; enumerator order and values + * must stay identical because the config layer persists them as ints. + */ +enum OakCommonDropWorkflowBehavior { + OAKCOMMON_DWS_ASK = 0, /**< Ask the user every time. */ + OAKCOMMON_DWS_AUTO = 1, /**< Automatically create a sequence. */ + OAKCOMMON_DWS_MANUAL = 2, /**< Never create; import manually. */ + OAKCOMMON_DWS_DISABLE = 3 /**< Disable dropping entirely. */ +}; + +/** + * @brief Check whether value is a valid OakCommonDropWorkflowBehavior. + * + * @param value Integer behavior value (e.g. read from config). + * @return 1 if valid, 0 otherwise (this is a predicate, not a status + * code). + */ +int oakcommon_drop_workflow_behavior_is_valid(int value); + +/** + * @brief Copy the printable name of a behavior into buf. + * + * Two-segment string getter: if buf is NULL or buf_size is too small, + * nothing is written. Invalid values yield "UNKNOWN". + * + * @param value One of OakCommonDropWorkflowBehavior. + * @param buf Destination buffer, may be NULL to query the size. + * @param buf_size Size of buf in bytes. + * @return Required buffer size in bytes including the terminating NUL + * (non-negative). + */ +int oakcommon_drop_workflow_behavior_name(int value, char *buf, + int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_DROPWORKFLOWBEHAVIOR_H diff --git a/include/common/error.h b/include/common/error.h new file mode 100644 index 000000000..cd8ad49c7 --- /dev/null +++ b/include/common/error.h @@ -0,0 +1,41 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_ERROR_H +#define OAK_EDITOR_ERROR_H + +/** + * @brief Status and error codes shared by all oakcommon C API families. + * + * Return-code convention (mirrors engine/include/oakengine/init.h): + * 0 (OAKCOMMON_OK) on success, a negative OAKCOMMON_E_* error code on + * failure. String getters return the required buffer size in bytes + * (including the terminating NUL) as a non-negative value instead. + */ +#define OAKCOMMON_OK 0 /**< Success. */ +#define OAKCOMMON_E_INVALID (-1) /**< NULL handle or invalid argument. */ +#define OAKCOMMON_E_STATE (-2) /**< Call not valid in the current state. */ +#define OAKCOMMON_E_FAILED (-3) /**< The underlying operation failed. */ +#define OAKCOMMON_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */ +#define OAKCOMMON_E_NOMEM (-5) /**< Allocation failed. */ + +#define SUCCESS OAKCOMMON_OK /**< @deprecated Use OAKCOMMON_OK. */ + +#endif //OAK_EDITOR_ERROR_H diff --git a/include/common/ffmpegutils.h b/include/common/ffmpegutils.h new file mode 100644 index 000000000..ee589b567 --- /dev/null +++ b/include/common/ffmpegutils.h @@ -0,0 +1,116 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_FFMPEGUTILS_H +#define OAK_EDITOR_FFMPEGUTILS_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Stateless mappings between native pixel/sample formats and the + * opaque FBPixelFormat / FBSampleFormat constants of ffmpeg_bridge + * + * All functions are pure format conversions; there is no handle to create + * or free. Native pixel/sample formats are passed as plain ints matching + * the olive::core::PixelFormat::Format / SampleFormat::Format enum values + * (invalid = -1). Bridge formats are the fb_pix_fmt_* / fb_sample_fmt_* + * constants from ffmpeg_bridge/ffmpeg_bridge.h. + */ + +/** + * @brief RGB / RGBA channel counts (flattened from VideoParams) + */ +#define OAKCOMMON_RGB_CHANNEL_COUNT 3 +#define OAKCOMMON_RGBA_CHANNEL_COUNT 4 + +/** + * @brief Returns a bridge pixel format that a frame can be converted to + * with minimal data loss, clamped to a maximum native precision + * + * @param pix_fmt bridge pixel format to find a compatible conversion for + * @param maximum_pix_fmt maximum native pixel format, or -1 for no limit + * @param out receives the chosen bridge pixel format + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL + */ +int oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( + int pix_fmt, int maximum_pix_fmt, int *out); + +/** + * @brief Returns a native pixel format usable to convert from a native + * frame to a bridge frame with minimal data loss + * + * @param pix_fmt native pixel format + * @param out receives the compatible native pixel format (-1 if none) + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL + */ +int oakcommon_ffmpegutils_get_compatible_pixel_format(int pix_fmt, + int *out); + +/** + * @brief Returns a bridge pixel format for a given native pixel format + * + * @param pix_fmt native pixel format + * @param channel_count OAKCOMMON_RGB_CHANNEL_COUNT or + * OAKCOMMON_RGBA_CHANNEL_COUNT + * @param out receives the bridge pixel format (fb_pix_fmt_none if none) + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL + */ +int oakcommon_ffmpegutils_get_ffmpeg_pixel_format(int pix_fmt, + int channel_count, + int *out); + +/** + * @brief Returns a native sample format for a given bridge sample format + * + * @param smp_fmt bridge sample format + * @param out receives the native sample format (-1 if unknown) + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL + */ +int oakcommon_ffmpegutils_get_native_sample_format(int smp_fmt, int *out); + +/** + * @brief Returns a bridge sample format for a given native sample format + * + * @param smp_fmt native sample format + * @param out receives the bridge sample format (fb_sample_fmt_none if none) + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL + */ +int oakcommon_ffmpegutils_get_ffmpeg_sample_format(int smp_fmt, int *out); + +/** + * @brief Converts a "JPEG" full-range bridge pixel format to its regular + * counterpart + * + * @param pix_fmt bridge pixel format + * @param out receives the regular-range format (unchanged if not JPEG) + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out is NULL + */ +int oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(int pix_fmt, + int *out); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_FFMPEGUTILS_H diff --git a/include/common/filefunctions.h b/include/common/filefunctions.h new file mode 100644 index 000000000..a627e03b7 --- /dev/null +++ b/include/common/filefunctions.h @@ -0,0 +1,153 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_FILEFUNCTIONS_H +#define OAK_EDITOR_FILEFUNCTIONS_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle for the filefunctions family + * + * File functions are stateless; the handle only exists to keep the C API + * shape uniform across oakcommon families. + */ +typedef struct OakCommonFileFunctions OakCommonFileFunctions; + +/** + * @brief Creates a filefunctions handle + * + * @return A new handle, or NULL on failure. + */ +OakCommonFileFunctions *oakcommon_filefunctions_init(void); + +/** + * @brief Destroys a filefunctions handle (NULL is a no-op) + */ +void oakcommon_filefunctions_free(OakCommonFileFunctions *self); + +/** + * @brief Returns a deterministic identifier string for a file + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. Returns an empty string (required size 1) if + * the file does not exist. + */ +int oakcommon_filefunctions_get_unique_file_identifier( + OakCommonFileFunctions *self, const char *filename, char *buf, + int buf_size); + +int oakcommon_filefunctions_get_configuration_location( + OakCommonFileFunctions *self, char *buf, int buf_size); + +int oakcommon_filefunctions_get_application_path( + OakCommonFileFunctions *self, char *buf, int buf_size); + +int oakcommon_filefunctions_get_temp_file_path( + OakCommonFileFunctions *self, char *buf, int buf_size); + +int oakcommon_filefunctions_get_auto_recovery_root( + OakCommonFileFunctions *self, char *buf, int buf_size); + +/** + * @brief Checks whether `source` can be copied to `dest` without + * overwriting anything + * + * @param out Receives 1 (safe) or 0 (would overwrite). + */ +int oakcommon_filefunctions_can_copy_directory_without_overwriting( + OakCommonFileFunctions *self, const char *source, const char *dest, + int *out); + +/** + * @brief Recursively copies a directory + */ +int oakcommon_filefunctions_copy_directory(OakCommonFileFunctions *self, + const char *source, + const char *dest, int overwrite); + +/** + * @brief Checks whether a directory exists, optionally creating it + * + * @param out Receives 1 (valid) or 0 (invalid). + */ +int oakcommon_filefunctions_directory_is_valid( + OakCommonFileFunctions *self, const char *dir, + int try_to_create_if_not_exists, int *out); + +/** + * @brief Ensures a filename ends with the given extension (no dot) + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. + */ +int oakcommon_filefunctions_ensure_filename_extension( + OakCommonFileFunctions *self, const char *filename, + const char *extension, char *buf, int buf_size); + +/** + * @brief Reads an entire file into a string + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. Returns an empty string (required size 1) if + * the file cannot be read. + */ +int oakcommon_filefunctions_read_file_as_string( + OakCommonFileFunctions *self, const char *filename, char *buf, + int buf_size); + +/** + * @brief Returns a non-existing temporary variant of `original` + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. + */ +int oakcommon_filefunctions_get_safe_temporary_filename( + OakCommonFileFunctions *self, const char *original, char *buf, + int buf_size); + +/** + * @brief Renames `from` to `to`, deleting `to` first if it exists + * + * @param out Receives 1 (renamed) or 0 (failed). + */ +int oakcommon_filefunctions_rename_file_allow_overwrite( + OakCommonFileFunctions *self, const char *from, const char *to, + int *out); + +/** + * @brief Appends the platform executable suffix (".exe" on Windows) + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. + */ +int oakcommon_filefunctions_get_formatted_executable_for_platform( + OakCommonFileFunctions *self, const char *unformatted, char *buf, + int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_FILEFUNCTIONS_H diff --git a/include/common/miscutils.h b/include/common/miscutils.h new file mode 100644 index 000000000..35df776b9 --- /dev/null +++ b/include/common/miscutils.h @@ -0,0 +1,117 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_MISCUTILS_H +#define OAK_EDITOR_MISCUTILS_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Minimum decibel value used by the editor (-200.0 dB). + * + * In basically all circumstances, this calculates to 0.0 linear. + */ +#define OAKCOMMON_DECIBEL_MINIMUM (-200.0) + +/** + * @brief Convert a linear amplitude to decibels. + * + * A linear value of 0.0 (or anything yielding an infinite result) returns + * OAKCOMMON_DECIBEL_MINIMUM. + * + * @param linear Linear amplitude. + * @param out_db Receives the decibel value. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_db is NULL. + */ +int oakcommon_decibel_from_linear(double linear, double *out_db); + +/** + * @brief Convert decibels to a linear amplitude. + * + * Results below 1e-6 are clamped to 0.0. + * + * @param db Decibel value. + * @param out_linear Receives the linear amplitude. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_linear is NULL. + */ +int oakcommon_decibel_to_linear(double db, double *out_linear); + +/** + * @brief Convert a logarithmic slider position (0..1) to decibels. + * + * @param logarithmic Logarithmic position. + * @param out_db Receives the decibel value. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_db is NULL. + */ +int oakcommon_decibel_from_logarithmic(double logarithmic, double *out_db); + +/** + * @brief Convert decibels to a logarithmic slider position (0..1). + * + * @param db Decibel value. + * @param out_logarithmic Receives the logarithmic position. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_logarithmic + * is NULL. + */ +int oakcommon_decibel_to_logarithmic(double db, double *out_logarithmic); + +/** + * @brief Convert a linear amplitude directly to a logarithmic position. + * + * @param linear Linear amplitude. + * @param out_logarithmic Receives the logarithmic position. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_logarithmic + * is NULL. + */ +int oakcommon_decibel_linear_to_logarithmic(double linear, + double *out_logarithmic); + +/** + * @brief Convert a logarithmic position directly to a linear amplitude. + * + * @param logarithmic Logarithmic position. + * @param out_linear Receives the linear amplitude. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_linear is NULL. + */ +int oakcommon_decibel_logarithmic_to_linear(double logarithmic, + double *out_linear); + +/** + * @brief Linearly interpolate between a and b using t. + * + * t should be between 0.0 and 1.0: 0.0 returns a, 1.0 returns b. + * + * @param a Start value. + * @param b End value. + * @param t Interpolation factor. + * @param out_value Receives the interpolated value. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_value is NULL. + */ +int oakcommon_lerp(double a, double b, double t, double *out_value); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_MISCUTILS_H diff --git a/include/common/ocioutils.h b/include/common/ocioutils.h new file mode 100644 index 000000000..6bad146c1 --- /dev/null +++ b/include/common/ocioutils.h @@ -0,0 +1,88 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_OCIOUTILS_H +#define OAK_EDITOR_OCIOUTILS_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Native pixel format codes, mirroring olive::core::PixelFormat + * + * The numeric values must stay in sync with + * olive/core/render/pixelformat.h (Format enum). + */ +enum OakCommonPixelFormat { + OAKCOMMON_PIXEL_FORMAT_INVALID = -1, /**< Invalid/unknown format. */ + OAKCOMMON_PIXEL_FORMAT_U8 = 0, /**< 8-bit unsigned integer. */ + OAKCOMMON_PIXEL_FORMAT_U10 = 1, /**< 10-bit unsigned integer. */ + OAKCOMMON_PIXEL_FORMAT_U16 = 2, /**< 16-bit unsigned integer. */ + OAKCOMMON_PIXEL_FORMAT_F16 = 3, /**< 16-bit float (half). */ + OAKCOMMON_PIXEL_FORMAT_F32 = 4, /**< 32-bit float. */ + OAKCOMMON_PIXEL_FORMAT_COUNT = 5 /**< Sentinel, not a valid format. */ +}; + +/** + * @brief OpenColorIO bit depth codes, matching OCIO::BitDepth + * + * Returned through the out parameter of + * oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format() as a plain + * int so that callers never see OCIO types. Values match the OCIO + * BitDepth enum: 0 = unknown, 1 = uint8, 2 = uint10, 3 = uint12, + * 4 = uint14, 5 = uint16, 6 = uint32, 7 = f16, 8 = f32 (OCIO v2). + */ +typedef struct OakCommonOCIOUtils OakCommonOCIOUtils; + +/** + * @brief Creates an OCIOUtils handle + * + * The object is stateless; the handle exists only to satisfy the C API + * lifetime contract. Returns NULL on failure. + */ +OakCommonOCIOUtils *oakcommon_ocioutils_init(void); + +/** + * @brief Destroys an OCIOUtils handle; no-op on NULL + */ +void oakcommon_ocioutils_free(OakCommonOCIOUtils *self); + +/** + * @brief Maps a native pixel format to an OCIO bit depth + * + * @param self handle from oakcommon_ocioutils_init() + * @param pixel_format one of the OakCommonPixelFormat values + * @param out_bit_depth receives the OCIO bit depth as an int (see the + * OakCommonOCIOUtils typedef documentation); set to 0 + * (BIT_DEPTH_UNKNOWN) for invalid formats + * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self or + * out_bit_depth is NULL or pixel_format is not a known code + */ +int oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + OakCommonOCIOUtils *self, int pixel_format, int *out_bit_depth); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_OCIOUTILS_H diff --git a/include/common/oiioutils.h b/include/common/oiioutils.h new file mode 100644 index 000000000..f7ae5a709 --- /dev/null +++ b/include/common/oiioutils.h @@ -0,0 +1,109 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_OIIOUTILS_H +#define OAK_EDITOR_OIIOUTILS_H + +#include "common/error.h" + +/* Reuses the OakCommonPixelFormat enum (mirroring + * olive::core::PixelFormat) rather than redefining it here. */ +#include "common/ocioutils.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief OIIO base type codes, matching OIIO::TypeDesc::BASETYPE + * + * Passed through the C API as plain ints so callers never see OIIO + * types. Values match the OIIO TypeDesc::BASETYPE enum: 0 = UNKNOWN, + * 1 = NONE, 2 = UINT8, 3 = INT8, 4 = UINT16, 5 = INT16, 6 = UINT32, + * 7 = INT32, 8 = UINT64, 9 = INT64, 10 = HALF, 11 = FLOAT, 12 = DOUBLE, + * 13 = STRING, 14 = PTR. OIIO >= 2.5 adds 15 = USTRINGHASH and shifts + * LASTBASE, so the exact LASTBASE value is version-dependent. + */ +typedef struct OakCommonOIIOUtils OakCommonOIIOUtils; + +/** + * @brief Creates an OIIOUtils handle + * + * The object is stateless; the handle exists only to satisfy the C API + * lifetime contract. Returns NULL on failure. + */ +OakCommonOIIOUtils *oakcommon_oiioutils_init(void); + +/** + * @brief Destroys an OIIOUtils handle; no-op on NULL + */ +void oakcommon_oiioutils_free(OakCommonOIIOUtils *self); + +/** + * @brief Maps a native pixel format to an OIIO base type + * + * @param self handle from oakcommon_oiioutils_init() + * @param pixel_format one of the OakCommonPixelFormat values + * @param out_base_type receives the OIIO base type as an int (see the + * OakCommonOIIOUtils typedef documentation); set to 0 + * (TypeDesc::UNKNOWN) for invalid or unmappable formats + * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self or out_base_type + * is NULL or pixel_format is not a known code + */ +int oakcommon_oiioutils_get_oiio_base_type_from_format( + OakCommonOIIOUtils *self, int pixel_format, int *out_base_type); + +/** + * @brief Maps an OIIO base type to a native pixel format + * + * @param self handle from oakcommon_oiioutils_init() + * @param base_type an OIIO TypeDesc::BASETYPE value as an int + * @param out_pixel_format receives one of the OakCommonPixelFormat + * values; set to OAKCOMMON_PIXEL_FORMAT_INVALID for unknown or + * unmappable base types + * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self or + * out_pixel_format is NULL or base_type is negative + */ +int oakcommon_oiioutils_get_format_from_oiio_basetype( + OakCommonOIIOUtils *self, int base_type, int *out_pixel_format); + +/** + * @brief Converts a PixelAspectRatio attribute value to a rational + * + * Flattened form of the former ImageSpec-based helper: the caller reads + * the "PixelAspectRatio" float attribute from the OIIO::ImageSpec + * (defaulting to 1.0 when absent) and passes it here. + * + * @param self handle from oakcommon_oiioutils_init() + * @param pixel_aspect_ratio the PixelAspectRatio attribute value + * @param out_numerator receives the rational numerator + * @param out_denominator receives the rational denominator + * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self, out_numerator + * or out_denominator is NULL + */ +int oakcommon_oiioutils_get_pixel_aspect_ratio( + OakCommonOIIOUtils *self, double pixel_aspect_ratio, int *out_numerator, + int *out_denominator); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_OIIOUTILS_H diff --git a/include/common/power.h b/include/common/power.h new file mode 100644 index 000000000..ccf54abba --- /dev/null +++ b/include/common/power.h @@ -0,0 +1,58 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_POWER_H +#define OAK_EDITOR_POWER_H + +#include + +#include "error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Round `value` up to the next power of two + * + * Stateless pure function, no handle required. Writes the result to `out`. + * + * @param value Input value. + * @param out Receives the rounded value. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if `out` is NULL. + */ +int oakcommon_power_ceil_to_power_of_2(uint32_t value, uint32_t *out); + +/** + * @brief Round `value` down to the nearest power of two + * + * Stateless pure function, no handle required. Writes the result to `out`. + * + * @param value Input value. + * @param out Receives the rounded value. Must not be NULL. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if `out` is NULL. + */ +int oakcommon_power_floor_to_power_of_2(uint32_t value, uint32_t *out); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_POWER_H diff --git a/include/common/qtutils.h b/include/common/qtutils.h new file mode 100644 index 000000000..c370e8a29 --- /dev/null +++ b/include/common/qtutils.h @@ -0,0 +1,67 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_COMMON_QTUTILS_H +#define OAK_COMMON_QTUTILS_H + +#include + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Convert a pointer to an integer value + * + * @param ptr Pointer to convert (may be NULL, yielding 0). + * @param out_value Receives the integer representation of ptr. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_value is NULL. + */ +int oakcommon_qtutils_ptr_to_value(void *ptr, uint64_t *out_value); + +/** + * @brief Convert an integer produced by oakcommon_qtutils_ptr_to_value() back to a pointer + * + * @param value Integer representation of a pointer. + * @param out_ptr Receives the decoded pointer. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_ptr is NULL. + */ +int oakcommon_qtutils_value_to_ptr(uint64_t value, void **out_ptr); + +/** + * @brief Get the creation (birth) time of a file as seconds since the Unix epoch + * + * Falls back to the last metadata change time when the filesystem does not + * record birth times. + * + * @param path NUL-terminated filesystem path. + * @param out_secs Receives the creation time in seconds since the epoch. + * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID for NULL arguments, + * OAKCOMMON_E_NOT_FOUND if the file does not exist or cannot be stat'ed. + */ +int oakcommon_qtutils_get_creation_date(const char *path, int64_t *out_secs); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_COMMON_QTUTILS_H diff --git a/include/common/xmlutils.h b/include/common/xmlutils.h new file mode 100644 index 000000000..e83ae0194 --- /dev/null +++ b/include/common/xmlutils.h @@ -0,0 +1,156 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_XMLUTILS_H +#define OAK_EDITOR_XMLUTILS_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct OakCommonXmlReader OakCommonXmlReader; +typedef struct OakCommonXmlWriter OakCommonXmlWriter; + +/** + * @brief Create a streaming XML reader over a complete document. + * + * @param data NUL-terminated XML text. Must not be NULL. + * @return A new reader, or NULL on failure (NULL data, out of memory). + */ +OakCommonXmlReader *oakcommon_xml_reader_init(const char *data); + +/** + * @brief Destroy a reader. No-op on NULL. + */ +void oakcommon_xml_reader_free(OakCommonXmlReader *reader); + +/** + * @brief Advance until the next start element, an end element, or the end + * of the document. + * + * @param reader Reader handle. + * @param found Out: 1 if positioned on a start element, 0 otherwise. + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_xml_reader_read_next_start_element(OakCommonXmlReader *reader, + int *found); + +/** + * @brief Name of the current element token. + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. + */ +int oakcommon_xml_reader_name(OakCommonXmlReader *reader, char *buf, + int buf_size); + +/** + * @brief Read the concatenated character data of the current element. + * + * Must be called on a start element; consumes up to the matching end + * element. + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. + */ +int oakcommon_xml_reader_read_element_text(OakCommonXmlReader *reader, + char *buf, int buf_size); + +/** + * @brief Skip the current element and all of its children. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_xml_reader_skip_current_element(OakCommonXmlReader *reader); + +/** + * @brief Number of attributes on the current start element. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_xml_reader_attribute_count(OakCommonXmlReader *reader, + int *count); + +/** + * @brief Name of the attribute at @p index on the current start element. + * + * @return Required buffer size in bytes (including NUL), OAKCOMMON_E_NOT_FOUND + * if @p index is out of range, or another negative OAKCOMMON_E_* code. + */ +int oakcommon_xml_reader_attribute_name(OakCommonXmlReader *reader, int index, + char *buf, int buf_size); + +/** + * @brief Value of the attribute at @p index on the current start element. + * + * @return Required buffer size in bytes (including NUL), OAKCOMMON_E_NOT_FOUND + * if @p index is out of range, or another negative OAKCOMMON_E_* code. + */ +int oakcommon_xml_reader_attribute_value(OakCommonXmlReader *reader, + int index, char *buf, int buf_size); + +/** + * @brief Whether the document failed to parse. + * + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_xml_reader_has_error(OakCommonXmlReader *reader, + int *has_error); + +/** + * @brief Create a streaming XML writer. + * + * @return A new writer, or NULL on failure. + */ +OakCommonXmlWriter *oakcommon_xml_writer_init(void); + +/** + * @brief Destroy a writer. No-op on NULL. + */ +void oakcommon_xml_writer_free(OakCommonXmlWriter *writer); + +int oakcommon_xml_writer_write_start_element(OakCommonXmlWriter *writer, + const char *name); +int oakcommon_xml_writer_write_attribute(OakCommonXmlWriter *writer, + const char *name, const char *value); +int oakcommon_xml_writer_write_characters(OakCommonXmlWriter *writer, + const char *text); +int oakcommon_xml_writer_write_text_element(OakCommonXmlWriter *writer, + const char *name, + const char *text); +int oakcommon_xml_writer_write_end_element(OakCommonXmlWriter *writer); +int oakcommon_xml_writer_write_end_document(OakCommonXmlWriter *writer); + +/** + * @brief The document written so far. + * + * @return Required buffer size in bytes (including NUL), or a negative + * OAKCOMMON_E_* error code. + */ +int oakcommon_xml_writer_output(OakCommonXmlWriter *writer, char *buf, + int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_XMLUTILS_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 000000000..094469010 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(common) \ No newline at end of file diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt new file mode 100644 index 000000000..f7d718e18 --- /dev/null +++ b/src/common/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(src) +add_subdirectory(c_api) + +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/src/common/c_api/CMakeLists.txt b/src/common/c_api/CMakeLists.txt new file mode 100644 index 000000000..28451f1f9 --- /dev/null +++ b/src/common/c_api/CMakeLists.txt @@ -0,0 +1,14 @@ +target_sources(oakcommon PRIVATE + commandlineparser.cpp + current.cpp + debug.cpp + dropworkflowbehavior.cpp + ffmpegutils.cpp + filefunctions.cpp + miscutils.cpp + ocioutils.cpp + oiioutils.cpp + power.cpp + qtutils.cpp + xmlutils.cpp +) diff --git a/src/common/c_api/commandlineparser.cpp b/src/common/c_api/commandlineparser.cpp new file mode 100644 index 000000000..56b522646 --- /dev/null +++ b/src/common/c_api/commandlineparser.cpp @@ -0,0 +1,282 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/commandlineparser.h" + +#include +#include +#include +#include + +#include "../src/commandlineparser.h" + +struct OakCommonCommandLineParser { + CommandLineParser impl; +}; + +struct OakCommonCommandLineOption { + CommandLineParser::Option *option; +}; + +struct OakCommonCommandLinePositionalArgument { + CommandLineParser::PositionalArgument *argument; +}; + +OakCommonCommandLineParser *oakcommon_commandlineparser_init(void) +{ + try { + return new (std::nothrow) OakCommonCommandLineParser(); + } catch (...) { + return NULL; + } +} + +void oakcommon_commandlineparser_free(OakCommonCommandLineParser *parser) +{ + delete parser; +} + +int oakcommon_commandlineparser_set_app_info(OakCommonCommandLineParser *parser, + const char *name, + const char *version) +{ + if (!parser || !name) { + return OAKCOMMON_E_INVALID; + } + + try { + parser->impl.set_app_info(name, version ? version : ""); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlineparser_add_option( + OakCommonCommandLineParser *parser, const char *const *names, int name_count, + const char *description, int takes_arg, const char *arg_placeholder, + int hidden, OakCommonCommandLineOption **out_option) +{ + if (!parser || !names || name_count <= 0) { + return OAKCOMMON_E_INVALID; + } + + try { + std::vector strings; + strings.reserve(static_cast(name_count)); + for (int i = 0; i < name_count; i++) { + if (!names[i]) { + return OAKCOMMON_E_INVALID; + } + strings.emplace_back(names[i]); + } + + const CommandLineParser::Option *option = parser->impl.add_option( + strings, description ? description : "", takes_arg != 0, + arg_placeholder ? arg_placeholder : "", hidden != 0); + + if (out_option) { + auto *handle = + new (std::nothrow) OakCommonCommandLineOption(); + if (!handle) { + return OAKCOMMON_E_NOMEM; + } + handle->option = const_cast(option); + *out_option = handle; + } + + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlineparser_add_positional_argument( + OakCommonCommandLineParser *parser, const char *name, + const char *description, int required, + OakCommonCommandLinePositionalArgument **out_argument) +{ + if (!parser || !name) { + return OAKCOMMON_E_INVALID; + } + + try { + const CommandLineParser::PositionalArgument *argument = + parser->impl.add_positional_argument( + name, description ? description : "", required != 0); + + if (out_argument) { + auto *handle = + new (std::nothrow) OakCommonCommandLinePositionalArgument(); + if (!handle) { + return OAKCOMMON_E_NOMEM; + } + handle->argument = + const_cast(argument); + *out_argument = handle; + } + + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlineparser_process(OakCommonCommandLineParser *parser, + const char *const *argv, int argc) +{ + if (!parser || !argv || argc < 0) { + return OAKCOMMON_E_INVALID; + } + + try { + std::vector args; + args.reserve(static_cast(argc)); + for (int i = 0; i < argc; i++) { + args.emplace_back(argv[i] ? argv[i] : ""); + } + + parser->impl.process(args); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlineparser_print_help(OakCommonCommandLineParser *parser, + const char *filename) +{ + if (!parser || !filename) { + return OAKCOMMON_E_INVALID; + } + + try { + parser->impl.print_help(filename); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlineoption_is_set(OakCommonCommandLineOption *option, + bool *is_set) +{ + if (!option || !option->option || !is_set) { + return OAKCOMMON_E_INVALID; + } + + try { + *is_set = option->option->is_set(); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +/** + * @brief Shared two-stage string getter. + * + * Returns the required buffer size in bytes (including the terminating + * NUL) as a non-negative value, or a negative error code. + */ +static int copy_setting(const std::string &value, char *buf, int buf_size) +{ + int required = static_cast(value.size()) + 1; + + if (buf && buf_size > 0) { + size_t copy_len = value.size(); + if (copy_len > static_cast(buf_size) - 1) { + copy_len = static_cast(buf_size) - 1; + } + memcpy(buf, value.data(), copy_len); + buf[copy_len] = '\0'; + } + + return required; +} + +int oakcommon_commandlineoption_get_setting(OakCommonCommandLineOption *option, + char *buf, int buf_size) +{ + if (!option || !option->option) { + return OAKCOMMON_E_INVALID; + } + + try { + return copy_setting(option->option->get_setting(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlineoption_set_setting(OakCommonCommandLineOption *option, + const char *value) +{ + if (!option || !option->option || !value) { + return OAKCOMMON_E_INVALID; + } + + try { + option->option->set_setting(value); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlinepositionalargument_get_setting( + OakCommonCommandLinePositionalArgument *argument, char *buf, int buf_size) +{ + if (!argument || !argument->argument) { + return OAKCOMMON_E_INVALID; + } + + try { + return copy_setting(argument->argument->get_setting(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_commandlinepositionalargument_set_setting( + OakCommonCommandLinePositionalArgument *argument, const char *value) +{ + if (!argument || !argument->argument || !value) { + return OAKCOMMON_E_INVALID; + } + + try { + argument->argument->set_setting(value); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +void oakcommon_commandlineoption_free(OakCommonCommandLineOption *option) +{ + delete option; +} + +void oakcommon_commandlinepositionalargument_free( + OakCommonCommandLinePositionalArgument *argument) +{ + delete argument; +} diff --git a/src/common/c_api/current.cpp b/src/common/c_api/current.cpp new file mode 100644 index 000000000..deeb53ca2 --- /dev/null +++ b/src/common/c_api/current.cpp @@ -0,0 +1,149 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/current.h" + +#include "../src/current.h" + +struct OakCommonCurrent { + Current *current; +}; + +OakCommonCurrent *oakcommon_current_instance(void) +{ + static OakCommonCurrent handle = { &Current::get_instance() }; + return &handle; +} + +void oakcommon_current_free(OakCommonCurrent *self) +{ + // No-op: the handle wraps a process-wide singleton. + (void)self; +} + +static int current_set(Current *current, + void (Current::*set_fn)(std::shared_ptr), + void *obj, OakCommonDestroyFn destroy) +{ + try { + std::shared_ptr value; + if (obj) { + if (destroy) + value = std::shared_ptr(obj, destroy); + else + value = std::shared_ptr( + obj, [](void *) {}); + } + (current->*set_fn)(value); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} + +static int current_get(Current *current, + std::shared_ptr (Current::*get_fn)() const, + void **out) +{ + try { + *out = (current->*get_fn)().get(); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} + +int oakcommon_current_set_video_params(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy) +{ + if (!self) + return OAKCOMMON_E_INVALID; + return current_set(self->current, &Current::set_current_video_params, + obj, destroy); +} + +int oakcommon_current_set_audio_params(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy) +{ + if (!self) + return OAKCOMMON_E_INVALID; + return current_set(self->current, &Current::set_current_audio_params, + obj, destroy); +} + +int oakcommon_current_set_plugin_host(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy) +{ + if (!self) + return OAKCOMMON_E_INVALID; + return current_set(self->current, &Current::set_plugin_host, obj, + destroy); +} + +int oakcommon_current_set_plugin_cache(OakCommonCurrent *self, void *obj, + OakCommonDestroyFn destroy) +{ + if (!self) + return OAKCOMMON_E_INVALID; + return current_set(self->current, &Current::set_plugin_cache, obj, + destroy); +} + +int oakcommon_current_get_video_params(OakCommonCurrent *self, void **out) +{ + if (!self || !out) + return OAKCOMMON_E_INVALID; + return current_get(self->current, &Current::current_video_params, + out); +} + +int oakcommon_current_get_audio_params(OakCommonCurrent *self, void **out) +{ + if (!self || !out) + return OAKCOMMON_E_INVALID; + return current_get(self->current, &Current::current_audio_params, + out); +} + +int oakcommon_current_get_plugin_host(OakCommonCurrent *self, void **out) +{ + if (!self || !out) + return OAKCOMMON_E_INVALID; + return current_get(self->current, &Current::plugin_host, out); +} + +int oakcommon_current_get_plugin_cache(OakCommonCurrent *self, void **out) +{ + if (!self || !out) + return OAKCOMMON_E_INVALID; + return current_get(self->current, &Current::plugin_cache, out); +} + +int oakcommon_current_is_interactive(OakCommonCurrent *self, int *out) +{ + if (!self || !out) + return OAKCOMMON_E_INVALID; + try { + *out = self->current->interactive() ? 1 : 0; + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} diff --git a/src/common/c_api/debug.cpp b/src/common/c_api/debug.cpp new file mode 100644 index 000000000..a669ec195 --- /dev/null +++ b/src/common/c_api/debug.cpp @@ -0,0 +1,45 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/debug.h" + +#include "../src/debug.h" + +int oakcommon_debug_log(int level, const char *msg) +{ + if (!msg) + return OAKCOMMON_E_INVALID; + + try { + olive::debug_handler(level, msg); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} + +int oakcommon_debug_level_name(int level, char *buf, int buf_size) +{ + try { + return olive::debug_level_name(level, buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/dropworkflowbehavior.cpp b/src/common/c_api/dropworkflowbehavior.cpp new file mode 100644 index 000000000..f4e0ac43b --- /dev/null +++ b/src/common/c_api/dropworkflowbehavior.cpp @@ -0,0 +1,66 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/dropworkflowbehavior.h" + +#include + +#include "../src/dropworkflowbehavior.h" + +static const char *behavior_name(int value) +{ + switch (value) { + case olive::k_dws_ask: + return "ASK"; + case olive::k_dws_auto: + return "AUTO"; + case olive::k_dws_manual: + return "MANUAL"; + case olive::k_dws_disable: + return "DISABLE"; + } + return "UNKNOWN"; +} + +int oakcommon_drop_workflow_behavior_is_valid(int value) +{ + switch (value) { + case olive::k_dws_ask: + case olive::k_dws_auto: + case olive::k_dws_manual: + case olive::k_dws_disable: + return 1; + } + return 0; +} + +int oakcommon_drop_workflow_behavior_name(int value, char *buf, int buf_size) +{ + try { + const char *name = behavior_name(value); + int needed = (int)strlen(name) + 1; + + if (buf && buf_size >= needed) + memcpy(buf, name, needed); + return needed; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/ffmpegutils.cpp b/src/common/c_api/ffmpegutils.cpp new file mode 100644 index 000000000..3cae515a4 --- /dev/null +++ b/src/common/c_api/ffmpegutils.cpp @@ -0,0 +1,118 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/ffmpegutils.h" + +#include "../src/ffmpegutils.h" + +int oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( + int pix_fmt, int maximum_pix_fmt, int *out) +{ + if (!out) + return OAKCOMMON_E_INVALID; + + try { + *out = olive::FFmpegUtils::get_compatible_bridge_pixel_format( + pix_fmt, + static_cast( + maximum_pix_fmt)); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + + return OAKCOMMON_OK; +} + +int oakcommon_ffmpegutils_get_compatible_pixel_format(int pix_fmt, int *out) +{ + if (!out) + return OAKCOMMON_E_INVALID; + + try { + *out = olive::FFmpegUtils::get_compatible_pixel_format( + static_cast(pix_fmt)); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + + return OAKCOMMON_OK; +} + +int oakcommon_ffmpegutils_get_ffmpeg_pixel_format(int pix_fmt, + int channel_count, int *out) +{ + if (!out) + return OAKCOMMON_E_INVALID; + + try { + *out = olive::FFmpegUtils::get_ffmpeg_pixel_format( + static_cast(pix_fmt), + channel_count); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + + return OAKCOMMON_OK; +} + +int oakcommon_ffmpegutils_get_native_sample_format(int smp_fmt, int *out) +{ + if (!out) + return OAKCOMMON_E_INVALID; + + try { + *out = olive::FFmpegUtils::get_native_sample_format(smp_fmt); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + + return OAKCOMMON_OK; +} + +int oakcommon_ffmpegutils_get_ffmpeg_sample_format(int smp_fmt, int *out) +{ + if (!out) + return OAKCOMMON_E_INVALID; + + try { + *out = olive::FFmpegUtils::get_ffmpeg_sample_format( + static_cast(smp_fmt)); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + + return OAKCOMMON_OK; +} + +int oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(int pix_fmt, + int *out) +{ + if (!out) + return OAKCOMMON_E_INVALID; + + try { + *out = olive::FFmpegUtils::convert_jpeg_space_to_regular_space( + pix_fmt); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + + return OAKCOMMON_OK; +} diff --git a/src/common/c_api/filefunctions.cpp b/src/common/c_api/filefunctions.cpp new file mode 100644 index 000000000..a4a888ee3 --- /dev/null +++ b/src/common/c_api/filefunctions.cpp @@ -0,0 +1,295 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/filefunctions.h" + +#include +#include + +#include "../src/filefunctions.h" + +struct OakCommonFileFunctions { + int unused; /**< Stateless family; handle kept for API uniformity. */ +}; + +namespace +{ + +/** + * @brief Copies `value` into the caller's two-stage buffer + * + * @return Required buffer size in bytes (including the terminating NUL). + * Always non-negative; error codes are handled by the callers. + */ +int write_string_result(const std::string &value, char *buf, int buf_size) +{ + int required = static_cast(value.size()) + 1; + if (buf != nullptr && buf_size >= required) { + memcpy(buf, value.c_str(), required); + } + return required; +} + +bool is_valid_string_out(const char *buf, int buf_size) +{ + return buf_size >= 0 && (buf_size == 0 || buf != nullptr); +} + +} // namespace + +OakCommonFileFunctions *oakcommon_filefunctions_init(void) +{ + try { + return new OakCommonFileFunctions{0}; + } catch (...) { + return nullptr; + } +} + +void oakcommon_filefunctions_free(OakCommonFileFunctions *self) +{ + delete self; +} + +int oakcommon_filefunctions_get_unique_file_identifier( + OakCommonFileFunctions *self, const char *filename, char *buf, + int buf_size) +{ + if (self == nullptr || filename == nullptr || + !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::get_unique_file_identifier(filename), buf, + buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_get_configuration_location( + OakCommonFileFunctions *self, char *buf, int buf_size) +{ + if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::get_configuration_location(), buf, + buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_get_application_path( + OakCommonFileFunctions *self, char *buf, int buf_size) +{ + if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result(FileFunctions::get_application_path(), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_get_temp_file_path( + OakCommonFileFunctions *self, char *buf, int buf_size) +{ + if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result(FileFunctions::get_temp_file_path(), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_get_auto_recovery_root( + OakCommonFileFunctions *self, char *buf, int buf_size) +{ + if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::get_auto_recovery_root(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_can_copy_directory_without_overwriting( + OakCommonFileFunctions *self, const char *source, const char *dest, + int *out) +{ + if (self == nullptr || source == nullptr || dest == nullptr || + out == nullptr) { + return OAKCOMMON_E_INVALID; + } + + try { + *out = FileFunctions::can_copy_directory_without_overwriting( + source, dest) + ? 1 + : 0; + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_copy_directory(OakCommonFileFunctions *self, + const char *source, + const char *dest, int overwrite) +{ + if (self == nullptr || source == nullptr || dest == nullptr) { + return OAKCOMMON_E_INVALID; + } + + try { + FileFunctions::copy_directory(source, dest, overwrite != 0); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_directory_is_valid( + OakCommonFileFunctions *self, const char *dir, + int try_to_create_if_not_exists, int *out) +{ + if (self == nullptr || dir == nullptr || out == nullptr) { + return OAKCOMMON_E_INVALID; + } + + try { + *out = FileFunctions::directory_is_valid( + dir, try_to_create_if_not_exists != 0) + ? 1 + : 0; + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_ensure_filename_extension( + OakCommonFileFunctions *self, const char *filename, + const char *extension, char *buf, int buf_size) +{ + if (self == nullptr || filename == nullptr || extension == nullptr || + !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::ensure_filename_extension(filename, + extension), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_read_file_as_string( + OakCommonFileFunctions *self, const char *filename, char *buf, + int buf_size) +{ + if (self == nullptr || filename == nullptr || + !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::read_file_as_string(filename), buf, + buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_get_safe_temporary_filename( + OakCommonFileFunctions *self, const char *original, char *buf, + int buf_size) +{ + if (self == nullptr || original == nullptr || + !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::get_safe_temporary_filename(original), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_rename_file_allow_overwrite( + OakCommonFileFunctions *self, const char *from, const char *to, + int *out) +{ + if (self == nullptr || from == nullptr || to == nullptr || + out == nullptr) { + return OAKCOMMON_E_INVALID; + } + + try { + *out = FileFunctions::rename_file_allow_overwrite(from, to) ? 1 + : 0; + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_filefunctions_get_formatted_executable_for_platform( + OakCommonFileFunctions *self, const char *unformatted, char *buf, + int buf_size) +{ + if (self == nullptr || unformatted == nullptr || + !is_valid_string_out(buf, buf_size)) { + return OAKCOMMON_E_INVALID; + } + + try { + return write_string_result( + FileFunctions::get_formatted_executable_for_platform( + unformatted), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/miscutils.cpp b/src/common/c_api/miscutils.cpp new file mode 100644 index 000000000..27a07da3f --- /dev/null +++ b/src/common/c_api/miscutils.cpp @@ -0,0 +1,117 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/miscutils.h" + +#include "../src/decibel.h" +#include "../src/lerp.h" + +int oakcommon_decibel_from_linear(double linear, double *out_db) +{ + if (!out_db) { + return OAKCOMMON_E_INVALID; + } + try { + *out_db = olive::Decibel::from_linear(linear); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_decibel_to_linear(double db, double *out_linear) +{ + if (!out_linear) { + return OAKCOMMON_E_INVALID; + } + try { + *out_linear = olive::Decibel::to_linear(db); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_decibel_from_logarithmic(double logarithmic, double *out_db) +{ + if (!out_db) { + return OAKCOMMON_E_INVALID; + } + try { + *out_db = olive::Decibel::from_logarithmic(logarithmic); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_decibel_to_logarithmic(double db, double *out_logarithmic) +{ + if (!out_logarithmic) { + return OAKCOMMON_E_INVALID; + } + try { + *out_logarithmic = olive::Decibel::to_logarithmic(db); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_decibel_linear_to_logarithmic(double linear, + double *out_logarithmic) +{ + if (!out_logarithmic) { + return OAKCOMMON_E_INVALID; + } + try { + *out_logarithmic = olive::Decibel::linear_to_logarithmic(linear); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_decibel_logarithmic_to_linear(double logarithmic, + double *out_linear) +{ + if (!out_linear) { + return OAKCOMMON_E_INVALID; + } + try { + *out_linear = olive::Decibel::logarithmic_to_linear(logarithmic); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_lerp(double a, double b, double t, double *out_value) +{ + if (!out_value) { + return OAKCOMMON_E_INVALID; + } + try { + *out_value = lerp(a, b, t); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/ocioutils.cpp b/src/common/c_api/ocioutils.cpp new file mode 100644 index 000000000..a2a498411 --- /dev/null +++ b/src/common/c_api/ocioutils.cpp @@ -0,0 +1,62 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/ocioutils.h" + +#include + +#include "../src/ocioutils.h" + +struct OakCommonOCIOUtils { + int unused; /**< Stateless; only the address matters. */ +}; + +OakCommonOCIOUtils *oakcommon_ocioutils_init(void) +{ + try { + return new (std::nothrow) OakCommonOCIOUtils{}; + } catch (...) { + return NULL; + } +} + +void oakcommon_ocioutils_free(OakCommonOCIOUtils *self) +{ + delete self; +} + +int oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + OakCommonOCIOUtils *self, int pixel_format, int *out_bit_depth) +{ + try { + if (self == NULL || out_bit_depth == NULL) + return OAKCOMMON_E_INVALID; + if (pixel_format < OAKCOMMON_PIXEL_FORMAT_INVALID || + pixel_format >= OAKCOMMON_PIXEL_FORMAT_COUNT) + return OAKCOMMON_E_INVALID; + + ocio::BitDepth depth = OCIOUtils::get_ocio_bit_depth_from_pixel_format( + static_cast(pixel_format)); + *out_bit_depth = static_cast(depth); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/oiioutils.cpp b/src/common/c_api/oiioutils.cpp new file mode 100644 index 000000000..bc27bda92 --- /dev/null +++ b/src/common/c_api/oiioutils.cpp @@ -0,0 +1,100 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/oiioutils.h" + +#include + +#include "../src/oiioutils.h" + +struct OakCommonOIIOUtils { + int unused; /**< Stateless; only the address matters. */ +}; + +OakCommonOIIOUtils *oakcommon_oiioutils_init(void) +{ + try { + return new (std::nothrow) OakCommonOIIOUtils{}; + } catch (...) { + return NULL; + } +} + +void oakcommon_oiioutils_free(OakCommonOIIOUtils *self) +{ + delete self; +} + +int oakcommon_oiioutils_get_oiio_base_type_from_format( + OakCommonOIIOUtils *self, int pixel_format, int *out_base_type) +{ + try { + if (self == NULL || out_base_type == NULL) + return OAKCOMMON_E_INVALID; + if (pixel_format < OAKCOMMON_PIXEL_FORMAT_INVALID || + pixel_format >= OAKCOMMON_PIXEL_FORMAT_COUNT) + return OAKCOMMON_E_INVALID; + + OIIO::TypeDesc::BASETYPE base_type = + OIIOUtils::get_oiio_base_type_from_format( + static_cast(pixel_format)); + *out_base_type = static_cast(base_type); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_oiioutils_get_format_from_oiio_basetype( + OakCommonOIIOUtils *self, int base_type, int *out_pixel_format) +{ + try { + if (self == NULL || out_pixel_format == NULL) + return OAKCOMMON_E_INVALID; + if (base_type < 0 || base_type >= OIIO::TypeDesc::LASTBASE) + return OAKCOMMON_E_INVALID; + + olive::core::PixelFormat format = + OIIOUtils::get_format_from_oiio_basetype( + static_cast(base_type)); + *out_pixel_format = static_cast(format); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_oiioutils_get_pixel_aspect_ratio( + OakCommonOIIOUtils *self, double pixel_aspect_ratio, int *out_numerator, + int *out_denominator) +{ + try { + if (self == NULL || out_numerator == NULL || out_denominator == NULL) + return OAKCOMMON_E_INVALID; + + olive::core::Rational par = + olive::core::Rational::from_double(pixel_aspect_ratio); + *out_numerator = par.numerator(); + *out_denominator = par.denominator(); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/power.cpp b/src/common/c_api/power.cpp new file mode 100644 index 000000000..41a3c6107 --- /dev/null +++ b/src/common/c_api/power.cpp @@ -0,0 +1,53 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "../../include/common/power.h" + +#include "../src/power.h" + +int oakcommon_power_ceil_to_power_of_2(uint32_t value, uint32_t *out) +{ + try { + if (!out) { + return OAKCOMMON_E_INVALID; + } + + *out = olive::ceil_to_power_of_2(value); + + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_power_floor_to_power_of_2(uint32_t value, uint32_t *out) +{ + try { + if (!out) { + return OAKCOMMON_E_INVALID; + } + + *out = olive::floor_to_power_of_2(value); + + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/qtutils.cpp b/src/common/c_api/qtutils.cpp new file mode 100644 index 000000000..ba4baf917 --- /dev/null +++ b/src/common/c_api/qtutils.cpp @@ -0,0 +1,74 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/qtutils.h" + +#include "../src/qtutils.h" + +int oakcommon_qtutils_ptr_to_value(void *ptr, uint64_t *out_value) +{ + if (!out_value) { + return OAKCOMMON_E_INVALID; + } + + try { + *out_value = static_cast(olive::QtUtils::ptr_to_value(ptr)); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_qtutils_value_to_ptr(uint64_t value, void **out_ptr) +{ + if (!out_ptr) { + return OAKCOMMON_E_INVALID; + } + + try { + *out_ptr = + olive::QtUtils::value_to_ptr(static_cast(value)); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_qtutils_get_creation_date(const char *path, int64_t *out_secs) +{ + if (!path || !out_secs) { + return OAKCOMMON_E_INVALID; + } + + try { + std::chrono::system_clock::time_point t = + olive::QtUtils::get_creation_date(std::filesystem::path(path)); + + if (t == std::chrono::system_clock::time_point{}) { + return OAKCOMMON_E_NOT_FOUND; + } + + *out_secs = static_cast( + std::chrono::system_clock::to_time_t(t)); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/xmlutils.cpp b/src/common/c_api/xmlutils.cpp new file mode 100644 index 000000000..be9a84390 --- /dev/null +++ b/src/common/c_api/xmlutils.cpp @@ -0,0 +1,297 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/xmlutils.h" + +#include +#include + +#include "../src/xmlutils.h" + +struct OakCommonXmlReader { + olive::XmlStreamReader reader; + std::string cached_text; + bool has_cached_text = false; + + explicit OakCommonXmlReader(const char *data) + : reader(data) + { + } +}; + +struct OakCommonXmlWriter { + olive::XmlStreamWriter writer; +}; + +namespace +{ + +/** + * @brief Copy @p value into the two-stage string buffer. + * + * @return Required buffer size in bytes (including NUL). The buffer is only + * written to when it is large enough. + */ +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int needed = static_cast(value.size()) + 1; + if (buf && buf_size >= needed) { + memcpy(buf, value.c_str(), needed); + } + return needed; +} + +} // namespace + +extern "C" { + +OakCommonXmlReader *oakcommon_xml_reader_init(const char *data) +{ + if (!data) + return nullptr; + try { + return new (std::nothrow) OakCommonXmlReader(data); + } catch (...) { + return nullptr; + } +} + +void oakcommon_xml_reader_free(OakCommonXmlReader *reader) +{ + delete reader; +} + +int oakcommon_xml_reader_read_next_start_element(OakCommonXmlReader *reader, + int *found) +{ + if (!reader || !found) + return OAKCOMMON_E_INVALID; + try { + reader->has_cached_text = false; + *found = olive::xml_read_next_start_element(&reader->reader) ? 1 : 0; + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_name(OakCommonXmlReader *reader, char *buf, + int buf_size) +{ + if (!reader) + return OAKCOMMON_E_INVALID; + try { + return copy_string(reader->reader.name(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_read_element_text(OakCommonXmlReader *reader, + char *buf, int buf_size) +{ + if (!reader) + return OAKCOMMON_E_INVALID; + try { + // read_element_text() consumes the stream, so cache the result to + // keep the two-stage (size query then copy) buffer convention working. + if (!reader->has_cached_text) { + reader->cached_text = reader->reader.read_element_text(); + reader->has_cached_text = true; + } + return copy_string(reader->cached_text, buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_skip_current_element(OakCommonXmlReader *reader) +{ + if (!reader) + return OAKCOMMON_E_INVALID; + try { + reader->has_cached_text = false; + reader->reader.skip_current_element(); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_attribute_count(OakCommonXmlReader *reader, + int *count) +{ + if (!reader || !count) + return OAKCOMMON_E_INVALID; + try { + *count = static_cast(reader->reader.attributes().size()); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_attribute_name(OakCommonXmlReader *reader, int index, + char *buf, int buf_size) +{ + if (!reader) + return OAKCOMMON_E_INVALID; + try { + const auto &attrs = reader->reader.attributes(); + if (index < 0 || index >= static_cast(attrs.size())) + return OAKCOMMON_E_NOT_FOUND; + return copy_string(attrs[index].name, buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_attribute_value(OakCommonXmlReader *reader, + int index, char *buf, int buf_size) +{ + if (!reader) + return OAKCOMMON_E_INVALID; + try { + const auto &attrs = reader->reader.attributes(); + if (index < 0 || index >= static_cast(attrs.size())) + return OAKCOMMON_E_NOT_FOUND; + return copy_string(attrs[index].value, buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_reader_has_error(OakCommonXmlReader *reader, + int *has_error) +{ + if (!reader || !has_error) + return OAKCOMMON_E_INVALID; + try { + *has_error = reader->reader.has_error() ? 1 : 0; + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +OakCommonXmlWriter *oakcommon_xml_writer_init(void) +{ + try { + return new (std::nothrow) OakCommonXmlWriter(); + } catch (...) { + return nullptr; + } +} + +void oakcommon_xml_writer_free(OakCommonXmlWriter *writer) +{ + delete writer; +} + +int oakcommon_xml_writer_write_start_element(OakCommonXmlWriter *writer, + const char *name) +{ + if (!writer || !name) + return OAKCOMMON_E_INVALID; + try { + writer->writer.write_start_element(name); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_writer_write_attribute(OakCommonXmlWriter *writer, + const char *name, const char *value) +{ + if (!writer || !name || !value) + return OAKCOMMON_E_INVALID; + try { + writer->writer.write_attribute(name, value); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_writer_write_characters(OakCommonXmlWriter *writer, + const char *text) +{ + if (!writer || !text) + return OAKCOMMON_E_INVALID; + try { + writer->writer.write_characters(text); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_writer_write_text_element(OakCommonXmlWriter *writer, + const char *name, + const char *text) +{ + if (!writer || !name || !text) + return OAKCOMMON_E_INVALID; + try { + writer->writer.write_text_element(name, text); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_writer_write_end_element(OakCommonXmlWriter *writer) +{ + if (!writer) + return OAKCOMMON_E_INVALID; + try { + writer->writer.write_end_element(); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_writer_write_end_document(OakCommonXmlWriter *writer) +{ + if (!writer) + return OAKCOMMON_E_INVALID; + try { + writer->writer.write_end_document(); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_xml_writer_output(OakCommonXmlWriter *writer, char *buf, + int buf_size) +{ + if (!writer) + return OAKCOMMON_E_INVALID; + try { + return copy_string(writer->writer.output(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +} // extern "C" diff --git a/src/common/src/CMakeLists.txt b/src/common/src/CMakeLists.txt new file mode 100644 index 000000000..db2868875 --- /dev/null +++ b/src/common/src/CMakeLists.txt @@ -0,0 +1,74 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# Modifications Copyright (C) 2025 mikesolar +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_library(oakcommon SHARED + commandlineparser.cpp + commandlineparser.h + current.cpp + current.h + debug.cpp + debug.h + decibel.h + define.h + dropworkflowbehavior.h + ffmpegutils.cpp + ffmpegutils.h + filefunctions.cpp + filefunctions.h + lerp.h + memorypool.h + ocioutils.cpp + ocioutils.h + oiioutils.cpp + oiioutils.h + power.h + qtutils.cpp + qtutils.h + threadsafemap.h + xmlutils.cpp + xmlutils.h +) + +# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone +# build (see src/common/standalone) sets OAK_REPO_ROOT explicitly. +if(NOT DEFINED OAK_REPO_ROOT) + set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR}) +endif() + +target_include_directories(oakcommon PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${OAK_REPO_ROOT}/include + ${OAK_REPO_ROOT}/third_party/openfx/include + ${OCIO_INCLUDE_DIRS} + ${OIIO_INCLUDE_DIRS} +) + +# Public headers of oakcommon include the olive/core wrapper headers +# (pixelformat.h, sampleformat.h, rational.h) and the ffmpeg_bridge C ABI +# header. Their include dirs are PRIVATE to those targets, so re-export them. +target_include_directories(oakcommon PUBLIC + ${OAK_REPO_ROOT}/core/include + ${OAK_REPO_ROOT}/ffmpeg_bridge/include +) + +target_link_libraries(oakcommon PUBLIC + olivecore + ffmpeg_bridge + EXPAT::EXPAT + ${OCIO_LIBRARIES} + ${OIIO_LIBRARIES} +) diff --git a/src/common/src/commandlineparser.cpp b/src/common/src/commandlineparser.cpp new file mode 100644 index 000000000..584418a3a --- /dev/null +++ b/src/common/src/commandlineparser.cpp @@ -0,0 +1,197 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "commandlineparser.h" + +#include +#include +#include + +CommandLineParser::~CommandLineParser() +{ + for (const KnownOption &o : options_) { + delete o.option; + } + + for (const KnownPositionalArgument &a : positional_args_) { + delete a.option; + } +} + +const CommandLineParser::Option * +CommandLineParser::add_option(const std::vector &strings, + const std::string &description, bool takes_arg, + const std::string &arg_placeholder, bool hidden) +{ + Option *o = new Option(); + + options_.push_back( + { strings, description, o, takes_arg, arg_placeholder, hidden }); + + return o; +} + +const CommandLineParser::PositionalArgument * +CommandLineParser::add_positional_argument(const std::string &name, + const std::string &description, + bool required) +{ + PositionalArgument *a = new PositionalArgument(); + + positional_args_.push_back({ name, description, a, required }); + + return a; +} + +static bool string_equals_case_insensitive(const std::string &a, + const std::string &b) +{ + if (a.size() != b.size()) { + return false; + } + + for (size_t i = 0; i < a.size(); i++) { + if (tolower(static_cast(a[i])) != + tolower(static_cast(b[i]))) { + return false; + } + } + + return true; +} + +void CommandLineParser::process(const std::vector &argv) +{ + size_t positional_index = 0; + + for (size_t i = 1; i < argv.size(); i++) { + if (!argv[i].empty() && argv[i][0] == '-') { + // Must be an option + + // Skip past first dash + std::string arg_basename = argv[i].substr(1); + + bool matched_known = false; + + for (KnownOption &o : options_) { + for (const std::string &s : o.args) { + if (string_equals_case_insensitive(s, arg_basename)) { + // Flag discovered! + o.option->set(); + + if (o.takes_arg && i + 1 < argv.size()) { + o.option->set_setting(argv[i + 1]); + i++; + } + + matched_known = true; + goto found_flag; + } + } + } + +found_flag: + if (!matched_known) { + fprintf(stderr, "Unknown parameter: %s\n", argv[i].c_str()); + } + + } else { + // Must be a positional flag + if (positional_index < positional_args_.size()) { + positional_args_[positional_index].option->set_setting(argv[i]); + positional_index++; + } else { + fprintf(stderr, "Unknown parameter: %s\n", argv[i].c_str()); + } + } + } +} + +void CommandLineParser::print_help(const char *filename) +{ + printf("%s %s\n", app_name_.c_str(), app_version_.c_str()); + + printf("Copyright (C) 2018-2022 Oak Video Editor Team\n"); + + std::string positional_args; + for (size_t i = 0; i < positional_args_.size(); i++) { + if (i > 0) { + positional_args.append(" "); + } + + positional_args.append("["); + positional_args.append(positional_args_[i].name); + positional_args.append("]"); + } + + const char *basename; +#ifdef _WIN32 + basename = strrchr(filename, '\\'); + if (!basename) { + basename = strrchr(filename, '/'); + } +#else + basename = strrchr(filename, '/'); +#endif + + if (basename) { + // Slash found, increment pointer to avoid showing the slash itself + basename++; + } else { + // If no slashes are found, assume string is already a basename + basename = filename; + } + + printf("Usage: %s [options] %s\n\n", basename, positional_args.c_str()); + for (const KnownOption &o : options_) { + if (o.hidden) { + continue; + } + + std::string all_args; + + for (size_t i = 0; i < o.args.size(); i++) { + if (i > 0) { + all_args.append(", "); + } + + all_args.append("-"); + all_args.append(o.args[i]); + } + + if (o.arg_placeholder.empty()) { + printf(" %s\n", all_args.c_str()); + } else { + printf(" %s <%s>\n", all_args.c_str(), o.arg_placeholder.c_str()); + } + + printf(" %s\n\n", o.description.c_str()); + } + + printf("\n"); +} + +void CommandLineParser::set_app_info(const std::string &name, + const std::string &version) +{ + app_name_ = name; + app_version_ = version; +} diff --git a/src/common/src/commandlineparser.h b/src/common/src/commandlineparser.h new file mode 100644 index 000000000..ebec7d472 --- /dev/null +++ b/src/common/src/commandlineparser.h @@ -0,0 +1,137 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_COMMANDLINEPARSER_H +#define OAK_COMMANDLINEPARSER_H + +#include +#include + +/** + * @brief Command-line argument parser + * + * You may be wondering why we don't use a library implementation like + * QCommandLineParser instead of a custom implementation like this. The + * reason why is because QCommandLineParser requires a QApplication object + * of some kind to already have been created before it can parse anything, + * but we need to be able to control whether a QApplication (GUI-mode) or + * a QCoreApplication (CLI-mode) is created which is set by the user as a + * command line argument. Therefore we needed a custom implementation that + * could parse arguments without the need for any application object to be + * present already. + */ +class CommandLineParser { +public: + CommandLineParser() = default; + ~CommandLineParser(); + + CommandLineParser(const CommandLineParser &) = delete; + CommandLineParser(CommandLineParser &&) = delete; + CommandLineParser &operator=(const CommandLineParser &) = delete; + CommandLineParser &operator=(CommandLineParser &&) = delete; + + class PositionalArgument { + public: + PositionalArgument() = default; + + const std::string &get_setting() const + { + return setting_; + } + + void set_setting(const std::string &s) + { + setting_ = s; + } + + private: + std::string setting_; + }; + + class Option : public PositionalArgument { + public: + Option() + { + is_set_ = false; + } + + bool is_set() const + { + return is_set_; + } + + void set() + { + is_set_ = true; + } + + private: + bool is_set_; + }; + + const Option *add_option(const std::vector &strings, + const std::string &description, bool takes_arg = false, + const std::string &arg_placeholder = std::string(), + bool hidden = false); + + const PositionalArgument *add_positional_argument(const std::string &name, + const std::string &description, + bool required = false); + + void process(const std::vector &argv); + + void print_help(const char *filename); + + /** + * @brief Set the application name/version shown by print_help() + * + * Replaces the former QCoreApplication::applicationName()/applicationVersion() + * dependency. Defaults to "oak" with an empty version. + */ + void set_app_info(const std::string &name, const std::string &version); + +private: + struct KnownOption { + std::vector args; + std::string description; + Option *option; + bool takes_arg; + std::string arg_placeholder; + bool hidden; + }; + + struct KnownPositionalArgument { + std::string name; + std::string description; + PositionalArgument *option; + bool required; + }; + + std::vector options_; + + std::vector positional_args_; + + std::string app_name_ = "oak"; + + std::string app_version_; +}; + +#endif // OAK_COMMANDLINEPARSER_H diff --git a/src/common/src/current.cpp b/src/common/src/current.cpp new file mode 100644 index 000000000..0ac2105dd --- /dev/null +++ b/src/common/src/current.cpp @@ -0,0 +1,23 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "current.h" + +Current Current::current_; diff --git a/src/common/src/current.h b/src/common/src/current.h new file mode 100644 index 000000000..81dea6dd4 --- /dev/null +++ b/src/common/src/current.h @@ -0,0 +1,95 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_CURRENT_H +#define OAK_CURRENT_H + +#include + +/** + * @brief Thread-shared holder for the "current" session-wide objects. + * + * De-Qt version of the old engine/common Current. The external objects + * (video/audio params, plugin host, plugin cache) live outside + * oakcommon, so they are kept as type-erased pointers + * (std::shared_ptr); callers recover the concrete type with + * std::static_pointer_cast. Slots may be empty (nullptr). + */ +class Current { +public: + static Current &get_instance() + { + return current_; + } + + std::shared_ptr current_video_params() const + { + return video_params_; + } + + std::shared_ptr current_audio_params() const + { + return audio_params_; + } + + void set_current_video_params(std::shared_ptr params) + { + video_params_ = params; + } + + void set_current_audio_params(std::shared_ptr params) + { + audio_params_ = params; + } + + bool interactive() const + { + return true; + } + + std::shared_ptr plugin_host() const + { + return plugin_host_; + } + + void set_plugin_host(std::shared_ptr host) + { + plugin_host_ = host; + } + + std::shared_ptr plugin_cache() const + { + return plugin_cache_; + } + + void set_plugin_cache(std::shared_ptr cache) + { + plugin_cache_ = cache; + } + +private: + static Current current_; + std::shared_ptr video_params_; + std::shared_ptr audio_params_; + std::shared_ptr plugin_host_; + std::shared_ptr plugin_cache_; +}; + +#endif // OAK_CURRENT_H diff --git a/src/common/src/debug.cpp b/src/common/src/debug.cpp new file mode 100644 index 000000000..555c8a743 --- /dev/null +++ b/src/common/src/debug.cpp @@ -0,0 +1,73 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "debug.h" + +#include +#include + +namespace olive +{ + +int debug_level_name(int level, char *buf, int buf_size) +{ + const char *name; + + switch (level) { + case k_debug_debug: + name = "DEBUG"; + break; + case k_debug_info: + name = "INFO"; + break; + case k_debug_warning: + name = "WARNING"; + break; + case k_debug_error: + name = "ERROR"; + break; + case k_debug_fatal: + name = "FATAL"; + break; + default: + name = "UNKNOWN"; + break; + } + + int needed = (int)strlen(name) + 1; + + if (buf && buf_size >= needed) + memcpy(buf, name, needed); + return needed; +} + +void debug_handler(int level, const char *msg) +{ + char level_name[16]; + + debug_level_name(level, level_name, sizeof(level_name)); + fprintf(stderr, "[%s] %s\n", level_name, msg ? msg : ""); + + // Always flush so debug messages appear immediately, even on + // platforms that buffer stderr. + fflush(stderr); +} + +} diff --git a/src/common/src/debug.h b/src/common/src/debug.h new file mode 100644 index 000000000..6c7224e8c --- /dev/null +++ b/src/common/src/debug.h @@ -0,0 +1,64 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DEBUG_H +#define OAK_DEBUG_H + +namespace olive +{ + +/** + * @brief Severity levels for debug output. + * + * Replaces Qt's QtMsgType now that the debug handler no longer depends + * on QDebug. + */ +enum DebugLevel { + k_debug_debug, + k_debug_info, + k_debug_warning, + k_debug_error, + k_debug_fatal +}; + +/** + * @brief Write the printable name of a debug level (e.g. "WARNING") into + * a caller-provided buffer. + * + * Two-stage form: pass `buf == NULL` or a too-small `buf_size` to query + * the required size. Writes "UNKNOWN" for levels outside the DebugLevel + * range. + * + * @return Required buffer size in bytes, including the terminating NUL. + */ +int debug_level_name(int level, char *buf, int buf_size); + +/** + * @brief Print a debug message to stderr, prefixed with its level. + * + * De-Qt replacement for the old Qt message handler: qDebug() output is + * replaced by fprintf(stderr). A NULL message is treated as an empty + * string. Lines are always flushed so messages appear immediately. + */ +void debug_handler(int level, const char *msg); + +} + +#endif // OAK_DEBUG_H diff --git a/src/common/src/decibel.h b/src/common/src/decibel.h new file mode 100644 index 000000000..e1ec48c99 --- /dev/null +++ b/src/common/src/decibel.h @@ -0,0 +1,96 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DECIBEL_H +#define OAK_DECIBEL_H + +#include +#include + +namespace olive +{ + +class Decibel { +public: + // In basically all circumstances, this should calculate to 0.0 linear + static constexpr double minimum = -200.0; + + static double from_linear(double linear) + { + double v = double(20.0) * std::log10(linear); + if (std::isinf(v)) { + return minimum; + } + return v; + } + + static double to_linear(double decibel) + { + double to_linear = std::pow(double(10.0), decibel / double(20.0)); + + // Minimum threshold that we figure is close enough to 0 that we may as well just return 0 + if (to_linear < 0.000001) { + return 0; + } else { + return to_linear; + } + } + + static double from_logarithmic(double logarithmic) + { + if (logarithmic < 0.001) + return minimum; + else if (logarithmic > 0.99) + return 0; + else + return 20.0 * std::log10(-std::log(1 - logarithmic) / lo_g100); + } + + static double to_logarithmic(double decibel) + { + if (std::abs(decibel) <= 1e-12) { + return 1; + } else { + return 1 - std::exp(-std::pow(10.0, decibel / 20.0) * lo_g100); + } + } + + static double linear_to_logarithmic(double linear) + { + return 1 - std::exp(-linear * lo_g100); + } + + static double logarithmic_to_linear(double logarithmic) + { + if (logarithmic > 0.99) { + return 1; + } else { + return -std::log(1 - logarithmic) / lo_g100; + } + } + +private: + static constexpr double lo_g100 = 4.60517018599; +}; + +} + +#endif // OAK_DECIBEL_H diff --git a/src/common/src/define.h b/src/common/src/define.h new file mode 100644 index 000000000..d917fca57 --- /dev/null +++ b/src/common/src/define.h @@ -0,0 +1,58 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OLIVECOMMONDEFINE_H +#define OAK_OLIVECOMMONDEFINE_H + +namespace olive +{ + +/// The minimum size an icon in ProjectExplorer can be +const int k_project_icon_size_minimum = 16; + +/// The maximum size an icon in ProjectExplorer can be +const int k_project_icon_size_maximum = 256; + +/// The default size an icon in ProjectExplorer can be +const int k_project_icon_size_default = 64; + +const int k_bytes_in_gigabyte = 1073741824; + +} + +/** + * Copy/move deleters. Similar to Q_DISABLE_COPY_MOVE, et al. but those functions are not present in Qt < 5.13 so we + * use our own functions for portability. + */ + +#define DISABLE_COPY(Class) \ + Class(const Class &) = delete; \ + Class &operator=(const Class &) = delete; + +#define DISABLE_MOVE(Class) \ + Class(Class &&) = delete; \ + Class &operator=(Class &&) = delete; + +#define DISABLE_COPY_MOVE(Class) \ + DISABLE_COPY(Class) \ + DISABLE_MOVE(Class) + +#endif // OAK_OLIVECOMMONDEFINE_H diff --git a/src/common/src/dropworkflowbehavior.h b/src/common/src/dropworkflowbehavior.h new file mode 100644 index 000000000..92a0ce3b1 --- /dev/null +++ b/src/common/src/dropworkflowbehavior.h @@ -0,0 +1,43 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DROPWORKFLOWBEHAVIOR_H +#define OAK_DROPWORKFLOWBEHAVIOR_H + +namespace olive +{ + +/** + * @brief Behavior when media is dropped onto a timeline without a sequence + * + * Shared by the config defaults (engine layer) and the timeline import + * tool (UI layer). Enumerator order matches the previous + * ImportTool::DropWithoutSequenceBehavior. + */ +enum DropWithoutSequenceBehavior { + k_dws_ask, + k_dws_auto, + k_dws_manual, + k_dws_disable +}; + +} + +#endif // OAK_DROPWORKFLOWBEHAVIOR_H diff --git a/src/common/src/ffmpegutils.cpp b/src/common/src/ffmpegutils.cpp new file mode 100644 index 000000000..7eb2b38eb --- /dev/null +++ b/src/common/src/ffmpegutils.cpp @@ -0,0 +1,197 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ffmpegutils.h" + +namespace olive +{ + +int FFmpegUtils::get_compatible_bridge_pixel_format(int pix_fmt, + PixelFormat maximum) +{ + int possible_pix_fmts[4]; + + possible_pix_fmts[0] = fb_pix_fmt_rgba; + + if (maximum == PixelFormat::u8) { + possible_pix_fmts[1] = fb_pix_fmt_none; + } else { + possible_pix_fmts[1] = fb_pix_fmt_rgb_a64_le; + if (maximum == PixelFormat::f32) { + possible_pix_fmts[2] = fb_pix_fmt_rgba_f32_le; + possible_pix_fmts[3] = fb_pix_fmt_none; + } else { + possible_pix_fmts[2] = fb_pix_fmt_none; + } + } + + return fb_find_best_pix_fmt_of_list(possible_pix_fmts, pix_fmt); +} + +SampleFormat FFmpegUtils::get_native_sample_format(int smp_fmt) +{ + switch (smp_fmt) { + case fb_sample_fmt_u8: + return SampleFormat::u8; + case fb_sample_fmt_s16: + return SampleFormat::s16; + case fb_sample_fmt_s32: + return SampleFormat::s32; + case fb_sample_fmt_s64: + return SampleFormat::s64; + case fb_sample_fmt_flt: + return SampleFormat::f32; + case fb_sample_fmt_dbl: + return SampleFormat::f64; + case fb_sample_fmt_u8_p: + return SampleFormat::u8_p; + case fb_sample_fmt_s16_p: + return SampleFormat::s16_p; + case fb_sample_fmt_s32_p: + return SampleFormat::s32_p; + case fb_sample_fmt_s64_p: + return SampleFormat::s64_p; + case fb_sample_fmt_fltp: + return SampleFormat::f32_p; + case fb_sample_fmt_dblp: + return SampleFormat::f64_p; + default: + break; + } + + return SampleFormat::invalid; +} + +int FFmpegUtils::get_ffmpeg_sample_format(const SampleFormat &smp_fmt) +{ + switch (smp_fmt) { + case SampleFormat::u8: + return fb_sample_fmt_u8; + case SampleFormat::s16: + return fb_sample_fmt_s16; + case SampleFormat::s32: + return fb_sample_fmt_s32; + case SampleFormat::s64: + return fb_sample_fmt_s64; + case SampleFormat::f32: + return fb_sample_fmt_flt; + case SampleFormat::f64: + return fb_sample_fmt_dbl; + case SampleFormat::u8_p: + return fb_sample_fmt_u8_p; + case SampleFormat::s16_p: + return fb_sample_fmt_s16_p; + case SampleFormat::s32_p: + return fb_sample_fmt_s32_p; + case SampleFormat::s64_p: + return fb_sample_fmt_s64_p; + case SampleFormat::f32_p: + return fb_sample_fmt_fltp; + case SampleFormat::f64_p: + return fb_sample_fmt_dblp; + case SampleFormat::invalid: + case SampleFormat::count: + break; + } + + return fb_sample_fmt_none; +} + +int FFmpegUtils::convert_jpeg_space_to_regular_space(int f) +{ + switch (f) { + case fb_pix_fmt_yuv_j420_p: + return fb_pix_fmt_yu_v420_p; + case fb_pix_fmt_yuv_j422_p: + return fb_pix_fmt_yu_v422_p; + case fb_pix_fmt_yuv_j444_p: + return fb_pix_fmt_yu_v444_p; + case fb_pix_fmt_yuv_j440_p: + return fb_pix_fmt_yu_v440_p; + case fb_pix_fmt_yuv_j411_p: + return fb_pix_fmt_yu_v411_p; + default: + break; + } + + return f; +} + +int FFmpegUtils::get_ffmpeg_pixel_format(const PixelFormat &pix_fmt, + int channel_layout) +{ + if (channel_layout == k_rgb_channel_count) { + switch (pix_fmt) { + case PixelFormat::u8: + return fb_pix_fmt_rg_b24; + case PixelFormat::u10: + return fb_pix_fmt_none; + case PixelFormat::u16: + return fb_pix_fmt_rg_b48_le; + case PixelFormat::f16: + return fb_pix_fmt_rgb_f16_le; + case PixelFormat::f32: + return fb_pix_fmt_rgb_f32_le; + case PixelFormat::invalid: + case PixelFormat::count: + break; + } + } else if (channel_layout == k_rgba_channel_count) { + switch (pix_fmt) { + case PixelFormat::u8: + return fb_pix_fmt_rgba; + case PixelFormat::u10: + return fb_pix_fmt_none; + case PixelFormat::u16: + return fb_pix_fmt_rgb_a64_le; + case PixelFormat::f16: + return fb_pix_fmt_rgba_f16_le; + case PixelFormat::f32: + return fb_pix_fmt_rgba_f32_le; + case PixelFormat::invalid: + case PixelFormat::count: + break; + } + } + + return fb_pix_fmt_none; +} + +PixelFormat FFmpegUtils::get_compatible_pixel_format(const PixelFormat &pix_fmt) +{ + switch (pix_fmt) { + case PixelFormat::u8: + return PixelFormat::u8; + case PixelFormat::u10: + return PixelFormat::u8; + case PixelFormat::u16: + case PixelFormat::f16: + case PixelFormat::f32: + return PixelFormat::u16; + case PixelFormat::invalid: + case PixelFormat::count: + break; + } + + return PixelFormat::invalid; +} + +} diff --git a/src/common/src/ffmpegutils.h b/src/common/src/ffmpegutils.h new file mode 100644 index 000000000..17cfe8893 --- /dev/null +++ b/src/common/src/ffmpegutils.h @@ -0,0 +1,97 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FFMPEGABSTRACTION_H +#define OAK_FFMPEGABSTRACTION_H + +#include + +#include +#include + +namespace olive +{ + +using namespace core; + +/** + * @brief C++ adapter mapping Olive's native enums to bridge pixel/sample + * formats + * + * All "FFmpeg" formats here are actually the opaque FBPixelFormat / + * FBSampleFormat constants of the ffmpeg_bridge library; no FFmpeg header + * or structure is ever seen by the editor. + */ +class FFmpegUtils { +public: + /** + * @brief Channel counts formerly taken from VideoParams + * + * Flattened to plain constants here so that this header no longer + * depends on render/videoparams.h (common must not depend on render). + */ + static constexpr int k_rgb_channel_count = 3; + static constexpr int k_rgba_channel_count = 4; + + /** + * @brief Returns a bridge pixel format that can be used to convert a frame to a data type Olive supports with minimal data loss + * + * Named distinctly from the native PixelFormat overload below: with both + * taking a single argument, an unscoped enum argument would silently + * prefer an int overload over the PixelFormat one. + */ + static int get_compatible_bridge_pixel_format( + int pix_fmt, PixelFormat maximum = PixelFormat::invalid); + + /** + * @brief Returns a native pixel format that can be used to convert from a native frame to a bridge frame with minimal data loss + */ + static PixelFormat get_compatible_pixel_format(const PixelFormat &pix_fmt); + + /** + * @brief Returns a bridge pixel format for a given native pixel format + */ + static int get_ffmpeg_pixel_format(const PixelFormat &pix_fmt, + int channel_layout); + + /** + * @brief Returns a native sample format type for a given bridge sample format + */ + static SampleFormat get_native_sample_format(int smp_fmt); + + /** + * @brief Returns a bridge sample format type for a given native type + */ + static int get_ffmpeg_sample_format(const SampleFormat &smp_fmt); + + /** + * @brief Convert "JPEG"/full-range colorspace to its regular counterpart + * + * "JPEG "spaces are deprecated in favor of the regular space and setting `color_range`. For the + * time being, FFmpeg still uses these JPEG spaces, so for simplicity (since we *are* color_range + * aware), we use this function. + */ + static int convert_jpeg_space_to_regular_space(int f); +}; + +} + +#endif // OAK_FFMPEGABSTRACTION_H diff --git a/src/common/src/filefunctions.cpp b/src/common/src/filefunctions.cpp new file mode 100644 index 000000000..33043cd6a --- /dev/null +++ b/src/common/src/filefunctions.cpp @@ -0,0 +1,375 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "filefunctions.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +namespace fs = std::filesystem; + +namespace +{ + +/** + * @brief FNV-1a hash of a string, returned as lowercase hex + */ +std::string fnv1a_hex(const std::string &data) +{ + uint64_t hash = 14695981039346656037ULL; + for (unsigned char c : data) { + hash ^= c; + hash *= 1099511628211ULL; + } + + char buf[17]; + snprintf(buf, sizeof(buf), "%016llx", + static_cast(hash)); + return buf; +} + +/** + * @brief Case-insensitive check whether `s` ends with `suffix` + */ +bool ends_with_case_insensitive(const std::string &s, const std::string &suffix) +{ + if (suffix.size() > s.size()) { + return false; + } + + size_t offset = s.size() - suffix.size(); + for (size_t i = 0; i < suffix.size(); i++) { + char a = s[offset + i]; + char b = suffix[i]; + if (a >= 'A' && a <= 'Z') { + a += 'a' - 'A'; + } + if (b >= 'A' && b <= 'Z') { + b += 'a' - 'A'; + } + if (a != b) { + return false; + } + } + + return true; +} + +} // namespace + +std::string FileFunctions::get_unique_file_identifier( + const std::string &filename) +{ + std::error_code ec; + fs::path abs = fs::absolute(filename, ec); + if (ec || !fs::exists(abs, ec)) { + return std::string(); + } + + auto mtime = fs::last_write_time(abs, ec); + if (ec) { + return std::string(); + } + + std::string hash_input = abs.string(); + hash_input.append(std::to_string( + static_cast(mtime.time_since_epoch().count()))); + + return fnv1a_hex(hash_input); +} + +std::string FileFunctions::get_configuration_location() +{ + // Tests and tooling can redirect the configuration (and, since most + // locations derive from it, the cache/data) root. + const char *override_dir = getenv("OAK_CONFIG_DIR"); + if (override_dir != nullptr && override_dir[0] != '\0') { + std::error_code ec; + fs::create_directories(override_dir, ec); + return override_dir; + } + + if (is_portable()) { + return get_application_path(); + } + + fs::path config_root; +#ifdef __APPLE__ + const char *home = getenv("HOME"); + if (home != nullptr && home[0] != '\0') { + config_root = fs::path(home) / "Library" / "Application Support"; + } +#else + const char *xdg_config = getenv("XDG_CONFIG_HOME"); + if (xdg_config != nullptr && xdg_config[0] != '\0') { + config_root = xdg_config; + } else { + const char *home = getenv("HOME"); + if (home != nullptr && home[0] != '\0') { + config_root = fs::path(home) / ".config"; + } + } +#endif + + if (config_root.empty()) { + // Last resort: temp directory + config_root = fs::temp_directory_path(); + } + + fs::path config_dir = config_root / "oak"; + std::error_code ec; + fs::create_directories(config_dir, ec); + return config_dir.string(); +} + +bool FileFunctions::is_portable() +{ + std::error_code ec; + return fs::exists(fs::path(get_application_path()) / "portable", ec); +} + +std::string FileFunctions::get_application_path() +{ +#ifdef __APPLE__ + uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::string buf(size, '\0'); + if (_NSGetExecutablePath(buf.data(), &size) == 0) { + std::error_code ec; + fs::path p = fs::weakly_canonical(buf, ec); + if (!ec) { + return p.parent_path().string(); + } + } +#elif defined(__linux__) + std::error_code ec; + fs::path p = fs::read_symlink("/proc/self/exe", ec); + if (!ec) { + return p.parent_path().string(); + } +#endif + + // Fallback: current working directory + std::error_code ec; + fs::path cwd = fs::current_path(ec); + return ec ? std::string() : cwd.string(); +} + +std::string FileFunctions::get_temp_file_path() +{ + std::error_code ec; + fs::path temp_path = fs::temp_directory_path(ec) / "oak"; + if (ec) { + temp_path = fs::path(".") / "oak-temp"; + } + + // Ensure it exists + fs::create_directories(temp_path, ec); + + return temp_path.string(); +} + +bool FileFunctions::can_copy_directory_without_overwriting( + const std::string &source, const std::string &dest) +{ + std::error_code ec; + for (const auto &entry : fs::directory_iterator(source, ec)) { + fs::path dest_equivalent = fs::path(dest) / entry.path().filename(); + + if (entry.is_directory(ec)) { + if (!can_copy_directory_without_overwriting( + entry.path().string(), + dest_equivalent.string())) { + return false; + } + } else if (fs::exists(dest_equivalent, ec)) { + return false; + } + } + + return true; +} + +void FileFunctions::copy_directory(const std::string &source, + const std::string &dest, bool overwrite) +{ + std::error_code ec; + if (!fs::is_directory(source, ec)) { + fprintf(stderr, "Failed to copy directory, source %s didn't exist\n", + source.c_str()); + return; + } + + if (!fs::create_directories(dest, ec) && ec) { + fprintf(stderr, "Failed to create destination directory %s\n", + dest.c_str()); + return; + } + + for (const auto &entry : fs::directory_iterator(source, ec)) { + fs::path dest_file_path = + fs::path(dest) / entry.path().filename(); + + if (entry.is_directory(ec)) { + // Copy dir + copy_directory(entry.path().string(), + dest_file_path.string(), overwrite); + } else { + // Copy file + if (overwrite) { + fs::permissions(dest_file_path, + fs::perms::owner_write | + fs::perms::group_write | + fs::perms::others_write, + fs::perm_options::add, ec); + ec.clear(); + fs::remove(dest_file_path, ec); + ec.clear(); + } + + fs::copy_file(entry.path(), dest_file_path, + fs::copy_options::none, ec); + if (ec) { + fprintf(stderr, + "Failed to copy file %s to %s: %s\n", + entry.path().string().c_str(), + dest_file_path.string().c_str(), + ec.message().c_str()); + } + } + } +} + +bool FileFunctions::directory_is_valid(const std::string &dir, + bool try_to_create_if_not_exists) +{ + // Return whether the directory exists, or whether it could be created + // if it doesn't + std::error_code ec; + if (fs::is_directory(dir, ec)) { + return true; + } + + return try_to_create_if_not_exists && fs::create_directories(dir, ec); +} + +std::string FileFunctions::ensure_filename_extension( + std::string fn, const std::string &extension) +{ + // No-op if either input is empty + if (!fn.empty() && !extension.empty()) { + std::string extension_with_dot = "." + extension; + + if (!ends_with_case_insensitive(fn, extension_with_dot)) { + fn.append(extension_with_dot); + } + } + + return fn; +} + +std::string FileFunctions::read_file_as_string(const std::string &filename) +{ + std::ifstream f(filename, std::ios::in | std::ios::binary); + if (!f.is_open()) { + return std::string(); + } + + std::ostringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +std::string FileFunctions::get_safe_temporary_filename( + const std::string &original) +{ + int counter = 0; + + fs::path original_path(original); + fs::path dir = original_path.parent_path(); + std::string filename = original_path.filename().string(); + + // Split off the complete suffix (everything from the first dot), like + // QFileInfo::completeSuffix() + std::string basename = filename; + std::string complete_suffix; + size_t first_dot = filename.find('.'); + if (first_dot != std::string::npos) { + basename = filename.substr(0, first_dot); + complete_suffix = filename.substr(first_dot); + } + + fs::path temp_abs_path; + do { + temp_abs_path = + dir / (basename + ".tmp" + std::to_string(counter) + + complete_suffix); + counter++; + } while (fs::exists(temp_abs_path)); + + return temp_abs_path.string(); +} + +bool FileFunctions::rename_file_allow_overwrite(const std::string &from, + const std::string &to) +{ + std::error_code ec; + if (fs::exists(to, ec) && !fs::remove(to, ec)) { + fprintf(stderr, "Couldn't remove existing file %s for overwrite\n", + to.c_str()); + return false; + } + + // By this point, we can assume `to` either never existed or has now + // been deleted + fs::rename(from, to, ec); + if (ec) { + fprintf(stderr, "Failed to rename file %s to %s: %s\n", + from.c_str(), to.c_str(), ec.message().c_str()); + return false; + } + + return true; +} + +std::string FileFunctions::get_formatted_executable_for_platform( + std::string unformatted) +{ +#ifdef _WIN32 + unformatted.append(".exe"); +#endif + + return unformatted; +} + +std::string FileFunctions::get_auto_recovery_root() +{ + return (fs::path(get_configuration_location()) / "autorecovery") + .string(); +} diff --git a/src/common/src/filefunctions.h b/src/common/src/filefunctions.h new file mode 100644 index 000000000..9a77f3f08 --- /dev/null +++ b/src/common/src/filefunctions.h @@ -0,0 +1,111 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FILEFUNCTIONS_H +#define OAK_FILEFUNCTIONS_H + +#include + +/** + * @brief A collection of static file and directory functions + * + * Qt-free reimplementation of the former olive::FileFunctions. All paths + * are UTF-8 std::strings and all filesystem operations use std::filesystem. + */ +class FileFunctions { +public: + /** + * @brief Returns true if the application is running in portable mode + * + * In portable mode, any persistent configuration files should be made in + * a path relative to the application rather than in the user's home + * folder. + */ + static bool is_portable(); + + /** + * @brief Returns a deterministic identifier string for a file + * + * Derived from the absolute path and the last modification time. Returns + * an empty string if the file does not exist. Uses FNV-1a hashing rather + * than SHA-1; identifiers are only used as cache keys, not for security. + */ + static std::string get_unique_file_identifier(const std::string &filename); + + static std::string get_configuration_location(); + + static std::string get_application_path(); + + static std::string get_temp_file_path(); + + static bool can_copy_directory_without_overwriting( + const std::string &source, const std::string &dest); + + static void copy_directory(const std::string &source, + const std::string &dest, + bool overwrite = false); + + static bool directory_is_valid(const std::string &dir, + bool try_to_create_if_not_exists = true); + + /** + * @brief Ensures a given filename has a certain extension + * + * Checks if the filename has the extension provided and appends it if + * not. The extension is checked case-insensitive. The extension should + * be provided with no dot (e.g. "ove" rather than ".ove"). + * + * @return The filename provided either untouched or with the extension + * appended to it. + */ + static std::string ensure_filename_extension(std::string fn, + const std::string &extension); + + static std::string read_file_as_string(const std::string &filename); + + /** + * @brief Returns a temporary filename that can be used while writing + * rather than the original + * + * If overwriting a file, it's safest to write to a new file first and + * then only replace it at the end so that if the program crashes or the + * user cancels the save half way through, the original file is still + * intact. + * + * This function returns a slight variant of the filename provided that's + * guaranteed to not exist and therefore won't overwrite anything + * important. + */ + static std::string get_safe_temporary_filename(const std::string &original); + + /** + * @brief Renames a file from `from` to `to`, deleting `to` if such a + * file already exists first + */ + static bool rename_file_allow_overwrite(const std::string &from, + const std::string &to); + + static std::string get_formatted_executable_for_platform( + std::string unformatted); + + static std::string get_auto_recovery_root(); +}; + +#endif // OAK_FILEFUNCTIONS_H diff --git a/src/common/src/lerp.h b/src/common/src/lerp.h new file mode 100644 index 000000000..33eaa4380 --- /dev/null +++ b/src/common/src/lerp.h @@ -0,0 +1,41 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LERP_H +#define OAK_LERP_H + +/** + * @brief Linearly interpolate a value between a and b using t + * + * t should be a number between 0.0 and 1.0. 0.0 will return a, 1.0 will return b, and between will return a value + * in between a and b at that point linearly. + */ +template T lerp(T a, T b, double t) +{ + return (a * (1.0 - t)) + (b * t); +} + +template T lerp(T a, T b, float t) +{ + return (a * (1.0f - t)) + (b * t); +} + +#endif // OAK_LERP_H diff --git a/src/common/src/memorypool.h b/src/common/src/memorypool.h new file mode 100644 index 000000000..3a24aeb05 --- /dev/null +++ b/src/common/src/memorypool.h @@ -0,0 +1,440 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MEMORYPOOL_H +#define OAK_MEMORYPOOL_H + +#include +#include +#include +#include +#include +#include +#include + +#include "define.h" + +namespace olive +{ + +/** + * @brief MemoryPool base class + * + * A custom memory system that allocates several objects in a large chunk (as opposed to several small + * allocations). Improves performance and memory consumption. + * + * As a class, this base is usable by subclassing and overriding get_element_size() if elements are larger than + * one byte. The pool will then allocate `(element_count * get_element_size())` per arena. Arenas are allocated + * and destroyed on the fly - when an arena fills up, another is allocated. + * + * get() will return an ElementPtr. The original desired data can be accessed through ElementPtr::data(). This data + * will belong to the caller until ElementPtr goes out of scope and the memory is freed back into the pool. + * + * Empty arenas are NOT reaped automatically anymore (the Qt version used a QTimer driven by the Qt event loop). + * Call clear_empty_arenas() periodically from whatever scheduling mechanism the host application provides. + */ +class MemoryPool { +public: + /** + * @brief Constructor + * @param element_count + * + * Number of elements per arena + */ + MemoryPool(int element_count) + { + element_count_ = element_count; + } + + /** + * @brief Destructor + * + * Deletes all arenas. + */ + virtual ~MemoryPool() + { + clear(); + } + + DISABLE_COPY_MOVE(MemoryPool) + + /** + * @brief Clears all arenas, freeing all of their memory + * + * Note that this function is not safe, any elements that are still out there will be invalid + * and accessing them will cause a crash. You'll need to make sure all elements are already + * relinquished before then. + */ + void clear() + { + std::lock_guard locker(lock_); + + for (Arena *a : arenas_) { + delete a; + } + arenas_.clear(); + } + + /** + * @brief Returns whether any arenas are successfully allocated + */ + inline bool is_allocated() const + { + return !arenas_.empty(); + } + + /** + * @brief Returns current number of allocated arenas + */ + inline int get_arena_count() const + { + return arenas_.size(); + } + + /** + * @brief Deletes arenas that have been empty for at least kMaxEmptyArenaLife milliseconds + * + * In the Qt version this was a private slot invoked periodically by a QTimer. Without Qt it is now a + * public function that the caller is responsible for invoking (e.g. from a std::thread timer). + */ + void clear_empty_arenas() + { + std::lock_guard locker(lock_); + + const int64_t min_time = now_ms() - kMaxEmptyArenaLife; + + for (auto it = arenas_.begin(); it != arenas_.end();) { + Arena *arena = (*it); + + if (arena->get_usage_count() == 0 && + arena->time_arena_was_made_empty() <= min_time) { + delete arena; + it = arenas_.erase(it); + } else { + it++; + } + } + } + + class Arena; + + /** + * @brief A handle for a chunk of memory in an arena + * + * Calling get() on the pool or arena will return a shared pointer to an element which will contain a pointer to + * the desired object/data in data(). When Element is destroyed (i.e. when ElementPtr goes out of scope), the + * memory is released back into the pool so it can be used by another class. + */ + class Element { + public: + /** + * @brief Element Constructor + * + * There is no need to use this outside of the memory pool's internal functions. + */ + Element(Arena *parent, uint8_t *data) + { + parent_ = parent; + data_ = data; + accessed_ = now_ms(); + } + + /** + * @brief Element Destructor + * + * Automatically releases this element's memory back to the arena it was retrieved from. + */ + ~Element() + { + release(); + } + + DISABLE_COPY_MOVE(Element) + + /** + * @brief Access data represented in the pool + */ + inline uint8_t *data() const + { + return data_; + } + + inline const int64_t ×tamp() const + { + return timestamp_; + } + + inline void set_timestamp(const int64_t ×tamp) + { + timestamp_ = timestamp; + } + + /** + * @brief Register that this element has been accessed + * + * \see last_accessed() + */ + inline void access() + { + accessed_ = now_ms(); + } + + /** + * @brief Returns the last time `access()` was called on this function + * + * Useful for determining the relative age of an element (i.e. if it hasn't been accessed for a certain amount + * of time, it can probably be freed back into the pool). This requires all usages to call `access()`. + */ + inline const int64_t &last_accessed() const + { + return accessed_; + } + + void release() + { + if (data_) { + parent_->release(this); + data_ = nullptr; + } + } + + private: + Arena *parent_; + + uint8_t *data_; + + int64_t timestamp_; + + int64_t accessed_; + }; + + using ElementPtr = std::shared_ptr; + + /** + * @brief A memory pool arena - a subsection of memory + * + * The pool itself does not store memory, it stores "arenas". This is so that the pool can handle the situation of + * an arena becoming full with no more memory to lend. A pool can automatically allocate another arena and + * continue providing memory (and freeing arenas when they're no longer in use). + */ + class Arena { + public: + Arena(MemoryPool *parent) + { + parent_ = parent; + data_ = nullptr; + allocated_sz_ = 0; + empty_time_ = now_ms(); + } + + ~Arena() + { + std::list copy = lent_elements_; + for (Element *e : copy) { + e->release(); + } + + delete[] data_; + } + + DISABLE_COPY_MOVE(Arena) + + /** + * @brief Returns an element if there is free memory to do so + */ + ElementPtr get() + { + std::lock_guard locker(lock_); + + for (size_t i = 0; i < available_.size(); i++) { + if (available_.at(i)) { + // This buffer is available + available_[i] = false; + + ElementPtr e = std::make_shared( + this, + reinterpret_cast(data_ + i * element_sz_)); + lent_elements_.push_back(e.get()); + + return e; + } + } + + return nullptr; + } + + /** + * @brief Releases an element back into the pool for use elsewhere + */ + void release(Element *e) + { + std::lock_guard locker(lock_); + uintptr_t diff = reinterpret_cast(e->data()) - + reinterpret_cast(data_); + + size_t index = diff / element_sz_; + + available_[index] = true; + + lent_elements_.remove(e); + + if (lent_elements_.empty()) { + empty_time_ = now_ms(); + } + } + + int get_usage_count() + { + std::lock_guard locker(lock_); + return lent_elements_.size(); + } + + bool allocate(size_t ele_sz, size_t nb_elements) + { + if (is_allocated()) { + return true; + } + + element_sz_ = ele_sz; + + allocated_sz_ = element_sz_ * nb_elements; + + if ((data_ = new (std::nothrow) uint8_t[allocated_sz_])) { + available_.assign(nb_elements, true); + + return true; + } else { + available_.clear(); + + return false; + } + } + + inline int get_element_count() const + { + return available_.size(); + } + + inline bool is_allocated() const + { + return data_; + } + + inline int64_t time_arena_was_made_empty() + { + std::lock_guard locker(lock_); + return empty_time_; + } + + private: + MemoryPool *parent_; + + uint8_t *data_; + + size_t allocated_sz_; + + std::vector available_; + + std::mutex lock_; + + size_t element_sz_; + + std::list lent_elements_; + + int64_t empty_time_; + }; + + /** + * @brief Retrieves an element from an available arena + */ + ElementPtr get() + { + std::lock_guard locker(lock_); + + // Attempt to get an element from an arena + for (Arena *a : arenas_) { + ElementPtr e = a->get(); + + if (e) { + return e; + } + } + + // All arenas were empty, we'll need to create a new one + size_t ele_sz = get_element_size(); + + if (!ele_sz) { + fprintf(stderr, "Failed to create arena, element size was 0\n"); + return nullptr; + } + + if (element_count_ <= 0) { + fprintf(stderr, "Failed to create arena, element count was invalid: %d\n", + element_count_); + return nullptr; + } + + Arena *a = new Arena(this); + if (!a->allocate(ele_sz, element_count_)) { + fprintf(stderr, + "Failed to create arena, allocation failed. Out of memory?\n"); + delete a; + return nullptr; + } + + arenas_.push_back(a); + return a->get(); + } + +protected: + /** + * @brief The size of each element + * + * Override this to use a custom size (e.g. a char array where the element size is > 1) + */ + virtual size_t get_element_size() + { + return sizeof(uint8_t); + } + +private: + /** + * @brief Milliseconds since the epoch (replaces QDateTime::currentMSecsSinceEpoch()) + */ + static int64_t now_ms() + { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } + + int element_count_; + + std::list arenas_; + + std::mutex lock_; + + static const int64_t kMaxEmptyArenaLife = 5000; +}; + +} + +#endif // OAK_MEMORYPOOL_H diff --git a/src/common/src/ocioutils.cpp b/src/common/src/ocioutils.cpp new file mode 100644 index 000000000..40481d6ab --- /dev/null +++ b/src/common/src/ocioutils.cpp @@ -0,0 +1,43 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ocioutils.h" + +ocio::BitDepth OCIOUtils::get_ocio_bit_depth_from_pixel_format( + olive::core::PixelFormat format) +{ + switch (format) { + case olive::core::PixelFormat::u8: + return ocio::BIT_DEPTH_UINT8; + case olive::core::PixelFormat::u10: + return ocio::BIT_DEPTH_UINT10; + case olive::core::PixelFormat::u16: + return ocio::BIT_DEPTH_UINT16; + case olive::core::PixelFormat::f16: + return ocio::BIT_DEPTH_F16; + case olive::core::PixelFormat::f32: + return ocio::BIT_DEPTH_F32; + case olive::core::PixelFormat::invalid: + case olive::core::PixelFormat::count: + break; + } + + return ocio::BIT_DEPTH_UNKNOWN; +} diff --git a/src/common/src/ocioutils.h b/src/common/src/ocioutils.h new file mode 100644 index 000000000..31dac6678 --- /dev/null +++ b/src/common/src/ocioutils.h @@ -0,0 +1,47 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OCIOUTILS_H +#define OAK_OCIOUTILS_H + +#include +namespace ocio = OCIO_NAMESPACE; + +#include + +/** + * @brief A collection of static OpenColorIO helper functions + * + * Qt-free reimplementation of the former olive::OCIOUtils. The dependency + * on render/videoparams.h (which pulls in Qt) was replaced with the + * Qt-free olive/core/render/pixelformat.h, where PixelFormat is defined. + */ +class OCIOUtils { +public: + /** + * @brief Returns the OCIO bit depth matching a native pixel format + * + * Returns ocio::BIT_DEPTH_UNKNOWN for invalid or out-of-range formats. + */ + static ocio::BitDepth get_ocio_bit_depth_from_pixel_format( + olive::core::PixelFormat format); +}; + +#endif // OAK_OCIOUTILS_H diff --git a/src/common/src/oiioutils.cpp b/src/common/src/oiioutils.cpp new file mode 100644 index 000000000..4a4bbfca2 --- /dev/null +++ b/src/common/src/oiioutils.cpp @@ -0,0 +1,104 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "oiioutils.h" + +#include + +OIIO::TypeDesc::BASETYPE +OIIOUtils::get_oiio_base_type_from_format(olive::core::PixelFormat format) +{ + switch (format) { + case olive::core::PixelFormat::u8: + return OIIO::TypeDesc::UINT8; + case olive::core::PixelFormat::u10: + return OIIO::TypeDesc::UNKNOWN; + case olive::core::PixelFormat::u16: + return OIIO::TypeDesc::UINT16; + case olive::core::PixelFormat::f16: + return OIIO::TypeDesc::HALF; + case olive::core::PixelFormat::f32: + return OIIO::TypeDesc::FLOAT; + case olive::core::PixelFormat::invalid: + case olive::core::PixelFormat::count: + break; + } + + return OIIO::TypeDesc::UNKNOWN; +} + +olive::core::PixelFormat +OIIOUtils::get_format_from_oiio_basetype(OIIO::TypeDesc::BASETYPE type) +{ + switch (type) { + case OIIO::TypeDesc::UNKNOWN: + case OIIO::TypeDesc::NONE: +#if OIIO_VERSION >= 20500 + case OIIO::TypeDesc::USTRINGHASH: +#endif + default: + break; + + case OIIO::TypeDesc::INT8: + case OIIO::TypeDesc::INT16: + case OIIO::TypeDesc::INT32: + case OIIO::TypeDesc::UINT32: + case OIIO::TypeDesc::INT64: + case OIIO::TypeDesc::UINT64: + case OIIO::TypeDesc::STRING: + case OIIO::TypeDesc::PTR: + case OIIO::TypeDesc::LASTBASE: + case OIIO::TypeDesc::DOUBLE: + fprintf(stderr, "Tried to use unknown OIIO base type\n"); + break; + + case OIIO::TypeDesc::UINT8: + return olive::core::PixelFormat::u8; + case OIIO::TypeDesc::UINT16: + return olive::core::PixelFormat::u16; + case OIIO::TypeDesc::HALF: + return olive::core::PixelFormat::f16; + case OIIO::TypeDesc::FLOAT: + return olive::core::PixelFormat::f32; + } + + return olive::core::PixelFormat::invalid; +} + +olive::core::Rational +OIIOUtils::get_pixel_aspect_ratio_from_oiio(const OIIO::ImageSpec &spec) +{ + return olive::core::Rational::from_double( + spec.get_float_attribute("PixelAspectRatio", 1)); +} + +void OIIOUtils::frame_to_buffer(const void *data, int64_t linesize_bytes, + OIIO::ImageBuf *buf) +{ + buf->set_pixels(OIIO::ROI(), buf->spec().format, data, OIIO::AutoStride, + static_cast(linesize_bytes)); +} + +void OIIOUtils::buffer_to_frame(OIIO::ImageBuf *buf, void *data, + int64_t linesize_bytes) +{ + buf->get_pixels(OIIO::ROI(), buf->spec().format, data, OIIO::AutoStride, + static_cast(linesize_bytes)); +} diff --git a/src/common/src/oiioutils.h b/src/common/src/oiioutils.h new file mode 100644 index 000000000..32c1d6517 --- /dev/null +++ b/src/common/src/oiioutils.h @@ -0,0 +1,88 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OIIOUTILS_H +#define OAK_OIIOUTILS_H + +#include + +#include +#include + +#include +#include + +/** + * @brief A collection of static OpenImageIO helper functions + * + * Qt-free reimplementation of the former olive::OIIOUtils. The reverse + * dependencies on codec/frame.h and render/videoparams.h (which pull in + * Qt) were replaced with the Qt-free olive/core/render/pixelformat.h and + * olive/core/util/rational.h. The Frame-based helpers were flattened to + * raw data pointer + linesize, which is all they ever used from Frame. + */ +class OIIOUtils { +public: + /** + * @brief Returns the OIIO base type matching a native pixel format + * + * Returns OIIO::TypeDesc::UNKNOWN for invalid, out-of-range or + * unmappable formats (e.g. u10, which OIIO has no base type for). + */ + static OIIO::TypeDesc::BASETYPE + get_oiio_base_type_from_format(olive::core::PixelFormat format); + + /** + * @brief Returns the native pixel format matching an OIIO base type + * + * Returns olive::core::PixelFormat::invalid for unknown or unmappable + * base types. + */ + static olive::core::PixelFormat + get_format_from_oiio_basetype(OIIO::TypeDesc::BASETYPE type); + + /** + * @brief Reads the PixelAspectRatio attribute of an image spec + * + * Defaults to 1:1 when the attribute is not present. + */ + static olive::core::Rational + get_pixel_aspect_ratio_from_oiio(const OIIO::ImageSpec &spec); + + /** + * @brief Copies raw pixel data into an OIIO image buffer + * + * Flattened form of the former Frame-based frame_to_buffer(); pass + * Frame::const_data() and Frame::linesize_bytes() at the call site. + */ + static void frame_to_buffer(const void *data, int64_t linesize_bytes, + OIIO::ImageBuf *buf); + + /** + * @brief Copies an OIIO image buffer's pixels into raw memory + * + * Flattened form of the former Frame-based buffer_to_frame(); pass + * Frame::data() and Frame::linesize_bytes() at the call site. + */ + static void buffer_to_frame(OIIO::ImageBuf *buf, void *data, + int64_t linesize_bytes); +}; + +#endif // OAK_OIIOUTILS_H diff --git a/src/common/src/power.h b/src/common/src/power.h new file mode 100644 index 000000000..4a3b56dc1 --- /dev/null +++ b/src/common/src/power.h @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_POWER_H +#define OAK_POWER_H + +#include + +#include "define.h" + +namespace olive +{ + +inline uint32_t ceil_to_power_of_2(uint32_t v) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + + return v; +} + +inline uint32_t floor_to_power_of_2(uint32_t x) +{ + x = x | (x >> 1); + x = x | (x >> 2); + x = x | (x >> 4); + x = x | (x >> 8); + x = x | (x >> 16); + + return x - (x >> 1); +} + +} + +#endif // OAK_POWER_H diff --git a/src/common/src/qtutils.cpp b/src/common/src/qtutils.cpp new file mode 100644 index 000000000..7ebca6c6c --- /dev/null +++ b/src/common/src/qtutils.cpp @@ -0,0 +1,52 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "qtutils.h" + +#include + +namespace olive +{ + +std::chrono::system_clock::time_point +QtUtils::get_creation_date(const std::filesystem::path &path) +{ + struct stat st; + + if (stat(path.c_str(), &st) != 0) { + return std::chrono::system_clock::time_point{}; + } + +#if defined(__APPLE__) || defined(__FreeBSD__) + // Filesystems that record a birth time expose it as st_birthtimespec; + // fall back to the metadata change time when it is not set. + time_t t = st.st_birthtimespec.tv_sec; + if (t == 0 || t == -1) { + t = st.st_ctimespec.tv_sec; + } + return std::chrono::system_clock::from_time_t(t); +#else + // Portable fallback: st_ctime (metadata change time), matching the + // Qt implementation's birthTime()/metadataChangeTime() fallback. + return std::chrono::system_clock::from_time_t(st.st_ctime); +#endif +} + +} diff --git a/src/common/src/qtutils.h b/src/common/src/qtutils.h new file mode 100644 index 000000000..be9839168 --- /dev/null +++ b/src/common/src/qtutils.h @@ -0,0 +1,89 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_QTUTILS_H +#define OAK_QTUTILS_H + +#include +#include +#include + +namespace olive +{ + +/** + * @brief Qt-free replacement for the former Qt-based QtUtils helper class. + * + * Only the functions actually used by the engine are kept. The UI-bound + * helpers (font metrics, combo boxes, QFrame creation, QColor conversion) + * remain available to Qt callers through the original Qt version. + */ +class QtUtils { +public: + /** + * @brief Convert a pointer to an integer value that can be passed around as data + */ + static uintptr_t ptr_to_value(void *ptr) + { + return reinterpret_cast(ptr); + } + + /** + * @brief Convert an integer produced by ptr_to_value() back to a pointer of any kind + */ + template static T *value_to_ptr(uintptr_t value) + { + return reinterpret_cast(value); + } + + /** + * @brief Walk the parent chain of an object looking for an ancestor of type T + * + * Works with any object type that provides a parent() member returning a + * pointer to the same (or a base) type, e.g. QObject. + */ + template + static T *get_parent_of_type(Obj *child) + { + auto *t = child ? child->parent() : nullptr; + + while (t) { + if (T *p = dynamic_cast(t)) { + return p; + } + t = t->parent(); + } + + return nullptr; + } + + /** + * @brief Get the creation (birth) time of a file + * + * Falls back to the last metadata change / modification time on + * filesystems that do not record birth times. + */ + static std::chrono::system_clock::time_point + get_creation_date(const std::filesystem::path &path); +}; + +} + +#endif // OAK_QTUTILS_H diff --git a/src/common/src/threadsafemap.h b/src/common/src/threadsafemap.h new file mode 100644 index 000000000..821d2ded4 --- /dev/null +++ b/src/common/src/threadsafemap.h @@ -0,0 +1,82 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OAK_THREADSAFEMAP_H +#define OAK_THREADSAFEMAP_H + +#include +#include + +/** + * @brief A minimal mutex-guarded associative container + * + * Drop-in stdlib replacement for the former QMap/QMutex based template. + */ +template class ThreadSafeMap { +public: + ThreadSafeMap() = default; + + /** + * @brief Insert or overwrite the value stored under `key` + */ + void insert(const K &key, const V &value) + { + std::lock_guard locker(mutex_); + map_.insert_or_assign(key, value); + } + + /** + * @brief Returns whether `key` exists in the map + */ + bool contains(const K &key) const + { + std::lock_guard locker(mutex_); + return map_.find(key) != map_.end(); + } + + /** + * @brief Copy the value stored under `key` into `out` + * @return true if the key existed, false otherwise (`out` left untouched) + */ + bool get(const K &key, V *out) const + { + std::lock_guard locker(mutex_); + auto it = map_.find(key); + if (it == map_.end()) { + return false; + } + *out = it->second; + return true; + } + + /** + * @brief Returns the number of entries in the map + */ + size_t size() const + { + std::lock_guard locker(mutex_); + return map_.size(); + } + +private: + mutable std::mutex mutex_; + + std::map map_; +}; + +#endif // OAK_THREADSAFEMAP_H diff --git a/src/common/src/xmlutils.cpp b/src/common/src/xmlutils.cpp new file mode 100644 index 000000000..aaa2f32b0 --- /dev/null +++ b/src/common/src/xmlutils.cpp @@ -0,0 +1,294 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "xmlutils.h" + +#include +#include + +#include + +namespace olive +{ + +namespace +{ + +/** + * @brief Expat user-data used while parsing a document into an event list. + */ +struct ParseContext { + std::vector *events; + bool has_error; + std::string error_string; +}; + +void XMLCALL on_start_element(void *user_data, const XML_Char *name, + const XML_Char **atts) +{ + ParseContext *ctx = static_cast(user_data); + XmlStreamReader::Event ev; + ev.type = XmlStreamReader::StartElement; + ev.name = name; + for (int i = 0; atts[i]; i += 2) + ev.attributes.push_back({atts[i], atts[i + 1]}); + ctx->events->push_back(std::move(ev)); +} + +void XMLCALL on_end_element(void *user_data, const XML_Char *name) +{ + ParseContext *ctx = static_cast(user_data); + XmlStreamReader::Event ev; + ev.type = XmlStreamReader::EndElement; + ev.name = name; + ctx->events->push_back(std::move(ev)); +} + +void XMLCALL on_characters(void *user_data, const XML_Char *s, int len) +{ + ParseContext *ctx = static_cast(user_data); + if (!len) + return; + if (!ctx->events->empty() && + ctx->events->back().type == XmlStreamReader::Characters) { + ctx->events->back().text.append(s, len); + return; + } + XmlStreamReader::Event ev; + ev.type = XmlStreamReader::Characters; + ev.text.assign(s, len); + ctx->events->push_back(std::move(ev)); +} + +std::string escape_text(const std::string &in) +{ + std::string out; + out.reserve(in.size()); + for (char c : in) { + switch (c) { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + default: out += c; break; + } + } + return out; +} + +std::string escape_attribute(const std::string &in) +{ + std::string out; + out.reserve(in.size()); + for (char c : in) { + switch (c) { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + case '"': out += """; break; + default: out += c; break; + } + } + return out; +} + +} // namespace + +XmlStreamReader::XmlStreamReader(const std::string &data) + : pos_(0) + , token_(Invalid) + , has_error_(false) +{ + ParseContext ctx{&events_, false, std::string()}; + + XML_Parser parser = XML_ParserCreate(nullptr); + if (!parser) { + has_error_ = true; + error_string_ = "failed to create XML parser"; + return; + } + + XML_SetUserData(parser, &ctx); + XML_SetElementHandler(parser, on_start_element, on_end_element); + XML_SetCharacterDataHandler(parser, on_characters); + + if (XML_Parse(parser, data.data(), static_cast(data.size()), + XML_TRUE) == XML_STATUS_ERROR) { + char buf[256]; + snprintf(buf, sizeof(buf), "%s at line %lu", + XML_ErrorString(XML_GetErrorCode(parser)), + static_cast( + XML_GetCurrentLineNumber(parser))); + has_error_ = true; + error_string_ = buf; + } + + XML_ParserFree(parser); +} + +XmlStreamReader::TokenType XmlStreamReader::read_next() +{ + attributes_.clear(); + text_.clear(); + name_.clear(); + + if (has_error_) { + token_ = Invalid; + return token_; + } + + if (pos_ >= events_.size()) { + token_ = EndDocument; + return token_; + } + + const Event &ev = events_[pos_++]; + token_ = ev.type; + name_ = ev.name; + text_ = ev.text; + attributes_ = ev.attributes; + return token_; +} + +std::string XmlStreamReader::read_element_text() +{ + if (!is_start_element()) + return std::string(); + + std::string result; + int depth = 1; + while (true) { + TokenType t = read_next(); + if (t == Invalid || t == EndDocument) + break; + if (t == StartElement) { + depth++; + } else if (t == EndElement) { + if (--depth == 0) + break; + } else if (t == Characters && depth == 1) { + result += text_; + } + } + return result; +} + +void XmlStreamReader::skip_current_element() +{ + if (!is_start_element()) + return; + + int depth = 1; + while (true) { + TokenType t = read_next(); + if (t == Invalid || t == EndDocument) + break; + if (t == StartElement) + depth++; + else if (t == EndElement && --depth == 0) + break; + } +} + +XmlStreamWriter::XmlStreamWriter() + : open_start_tag_(false) +{ +} + +void XmlStreamWriter::write_start_element(const std::string &name) +{ + if (open_start_tag_) { + output_ += '>'; + open_start_tag_ = false; + } + output_ += '<'; + output_ += name; + stack_.push_back(name); + open_start_tag_ = true; +} + +void XmlStreamWriter::write_attribute(const std::string &name, + const std::string &value) +{ + if (!open_start_tag_) + return; + output_ += ' '; + output_ += name; + output_ += "=\""; + output_ += escape_attribute(value); + output_ += '"'; +} + +void XmlStreamWriter::write_characters(const std::string &text) +{ + if (open_start_tag_) { + output_ += '>'; + open_start_tag_ = false; + } + output_ += escape_text(text); +} + +void XmlStreamWriter::write_text_element(const std::string &name, + const std::string &text) +{ + write_start_element(name); + write_characters(text); + write_end_element(); +} + +void XmlStreamWriter::write_end_element() +{ + if (stack_.empty()) + return; + + const std::string name = stack_.back(); + stack_.pop_back(); + + if (open_start_tag_) { + output_ += "/>"; + open_start_tag_ = false; + } else { + output_ += "'; + } +} + +void XmlStreamWriter::write_end_document() +{ + while (!stack_.empty()) + write_end_element(); +} + +bool xml_read_next_start_element(XmlStreamReader *reader) +{ + XmlStreamReader::TokenType token; + + while ((token = reader->read_next()) != XmlStreamReader::Invalid && + token != XmlStreamReader::EndDocument) { + if (reader->is_end_element()) + return false; + if (reader->is_start_element()) + return true; + } + + return false; +} + +} diff --git a/src/common/src/xmlutils.h b/src/common/src/xmlutils.h new file mode 100644 index 000000000..8800a536b --- /dev/null +++ b/src/common/src/xmlutils.h @@ -0,0 +1,191 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_XMLUTILS_H +#define OAK_XMLUTILS_H + +#include +#include + +namespace olive +{ + +/** + * @brief A single XML attribute (name/value pair). + * + * De-Qt replacement for QXmlStreamAttribute. + */ +struct XmlStreamAttribute { + std::string name; /**< Attribute name. */ + std::string value; /**< Attribute value (entity references resolved). */ +}; + +/** + * @brief Minimal pull-style streaming XML reader (de-Qt replacement for QXmlStreamReader). + * + * Backed by expat. The whole document is parsed at construction time into an + * internal event list; navigation is done with read_next() and the state + * accessors, mirroring the QXmlStreamReader call patterns used by the engine + * (read_next_start_element loop, attribute iteration, read_element_text(), + * skip_current_element()). + * + * Only the subset of XML actually used by Oak project/config files is + * supported: elements, attributes, character data, comments and processing + * instructions (the latter two are skipped transparently by expat handler + * omission). Namespace processing is not performed. + */ +class XmlStreamReader +{ +public: + /** + * @brief Token types, mirroring QXmlStreamReader::TokenType subset. + */ + enum TokenType { + Invalid, /**< Parse error occurred. */ + StartElement, + EndElement, + Characters, + EndDocument + }; + + /** + * @brief Construct a reader over a complete XML document. + * + * @param data The full XML text. Parse errors are reported via + * has_error()/error_string() and make read_next() return Invalid. + */ + explicit XmlStreamReader(const std::string &data); + + /** + * @brief Advance to the next token. + * + * @return The new current token type. + */ + TokenType read_next(); + + TokenType token_type() const { return token_; } + bool is_start_element() const { return token_ == StartElement; } + bool is_end_element() const { return token_ == EndElement; } + bool is_characters() const { return token_ == Characters; } + + /** + * @brief Name of the current StartElement/EndElement token. + */ + const std::string &name() const { return name_; } + + /** + * @brief Text of the current Characters token. + */ + const std::string &text() const { return text_; } + + /** + * @brief Attributes of the current StartElement token. + */ + const std::vector &attributes() const { return attributes_; } + + /** + * @brief Read the concatenated character data of the current element. + * + * Must be called on a StartElement token. Consumes tokens up to and + * including the matching EndElement. Mirrors + * QXmlStreamReader::readElementText(). + * + * @return The element's text content. + */ + std::string read_element_text(); + + /** + * @brief Skip the current element and all of its children. + * + * Mirrors QXmlStreamReader::skipCurrentElement(). + */ + void skip_current_element(); + + bool has_error() const { return has_error_; } + const std::string &error_string() const { return error_string_; } + + /** + * @brief One parsed SAX event. Internal; exposed for the expat handlers. + */ + struct Event { + TokenType type; + std::string name; + std::string text; + std::vector attributes; + }; + +private: + std::vector events_; /**< Parsed event list. */ + size_t pos_; /**< Index of the next event to consume. */ + TokenType token_; /**< Current token type. */ + std::string name_; /**< Current element name. */ + std::string text_; /**< Current character data. */ + std::vector attributes_; /**< Current attributes. */ + bool has_error_; + std::string error_string_; +}; + +/** + * @brief Minimal streaming XML writer (de-Qt replacement for QXmlStreamWriter). + * + * Only the functions actually used by Oak serializers are provided: + * write_start_element/write_attribute/write_characters/write_text_element/ + * write_end_element/write_end_document. Text and attribute values are escaped + * for the five predefined XML entities. + */ +class XmlStreamWriter +{ +public: + XmlStreamWriter(); + + void write_start_element(const std::string &name); + void write_attribute(const std::string &name, const std::string &value); + void write_characters(const std::string &text); + void write_text_element(const std::string &name, const std::string &text); + void write_end_element(); + void write_end_document(); + + /** + * @brief The document written so far. + */ + const std::string &output() const { return output_; } + +private: + std::string output_; /**< Accumulated document text. */ + std::vector stack_; /**< Open element name stack. */ + bool open_start_tag_; /**< A '>' is pending for the open tag. */ +}; + +/** + * @brief Workaround for read-next-start-element not detecting the end of a document. + * + * De-Qt port of the original xml_read_next_start_element(). Reads until a + * start element is found (returns true), an end element or the end of the + * document is reached (returns false). The old cancel_atom parameter (an + * engine/render dependency) has been removed. + * + * @param reader The reader to advance. + * @return true if positioned on a start element, false otherwise. + */ +bool xml_read_next_start_element(XmlStreamReader *reader); + +} + +#endif // OAK_XMLUTILS_H diff --git a/src/common/standalone/CMakeLists.txt b/src/common/standalone/CMakeLists.txt new file mode 100644 index 000000000..cd8dd6384 --- /dev/null +++ b/src/common/standalone/CMakeLists.txt @@ -0,0 +1,70 @@ +# Oak - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Standalone build driver for the oakcommon module. Builds only oakcommon, +# its tests, and the in-repo libraries it links against (olivecore, +# ffmpeg_bridge), without configuring the heavy top-level project. +# +# Usage (macOS/Homebrew): +# cmake -S src/common/standalone -B build-oakcommon +# cmake --build build-oakcommon -j +# ctest --test-dir build-oakcommon + +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) + +project(oakcommon-standalone LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +get_filename_component(OAK_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) + +list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake") +if(EXISTS "/opt/homebrew") + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") +endif() + +# Third-party dependencies. The top-level project resolves OCIO/OIIO through +# cmake/FindOpenColorIO.cmake / FindOpenImageIO.cmake (which are .so-centric); +# here we use the upstream CMake config packages shipped by Homebrew and map +# them onto the variables oakcommon's CMakeLists consumes. +find_package(EXPAT REQUIRED) +find_package(OpenColorIO CONFIG REQUIRED) +find_package(OpenImageIO CONFIG REQUIRED) + +set(OCIO_LIBRARIES OpenColorIO::OpenColorIO) +set(OCIO_INCLUDE_DIRS "") +set(OIIO_LIBRARIES OpenImageIO::OpenImageIO) +set(OIIO_INCLUDE_DIRS "") + +# In-repo libraries oakcommon links against: +# - olivecore: olive::core::Rational wraps the oakcore_* C ABI (real symbols) +# - ffmpeg_bridge: fb_find_best_pix_fmt_of_list (real symbol) +set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE) +# The /opt/otio OpenTimelineIO dylibs use @loader_path install names that +# break when linked from a build tree; oakcommon does not need OTIO. +set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON) + +add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core) +# The top-level project puts third_party/openfx/include on the global +# include path; replicate it here for olivecore's pixelformat.h (ofxCore.h). +target_include_directories(olivecore PUBLIC ${OAK_REPO_ROOT}/third_party/openfx/include) +add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge) + +set(BUILD_TESTING ON) +enable_testing() +add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common) diff --git a/src/common/tests/CMakeLists.txt b/src/common/tests/CMakeLists.txt new file mode 100644 index 000000000..6c2e65719 --- /dev/null +++ b/src/common/tests/CMakeLists.txt @@ -0,0 +1,44 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# Modifications Copyright (C) 2025 mikesolar +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +find_package(GTest REQUIRED) +include(GoogleTest) + +add_executable(oakcommon-gtest + commandlineparser_test.cpp + current_test.cpp + debug_test.cpp + dropworkflowbehavior_test.cpp + ffmpegutils_test.cpp + filefunctions_test.cpp + memorypool_test.cpp + miscutils_test.cpp + ocioutils_test.cpp + oiioutils_test.cpp + power_test.cpp + qtutils_test.cpp + threadsafemap_test.cpp + xmlutils_test.cpp +) + +target_link_libraries(oakcommon-gtest PRIVATE + oakcommon + GTest::gtest + GTest::gtest_main +) + +gtest_discover_tests(oakcommon-gtest) diff --git a/src/common/tests/commandlineparser_test.cpp b/src/common/tests/commandlineparser_test.cpp new file mode 100644 index 000000000..346e0a422 --- /dev/null +++ b/src/common/tests/commandlineparser_test.cpp @@ -0,0 +1,275 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include +#include + +#include + +#include "common/commandlineparser.h" + +TEST(CommandLineParser, InitFree) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, FreeNull) +{ + oakcommon_commandlineparser_free(nullptr); + oakcommon_commandlineoption_free(nullptr); + oakcommon_commandlinepositionalargument_free(nullptr); +} + +TEST(CommandLineParser, AddOptionInvalidArgs) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + const char *names[] = { "h", "-help" }; + OakCommonCommandLineOption *option = nullptr; + + EXPECT_EQ(oakcommon_commandlineparser_add_option( + nullptr, names, 2, "desc", 0, nullptr, 0, &option), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_commandlineparser_add_option( + parser, nullptr, 2, "desc", 0, nullptr, 0, &option), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_commandlineparser_add_option( + parser, names, 0, "desc", 0, nullptr, 0, &option), + OAKCOMMON_E_INVALID); + + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, ProcessOptionHitAndMiss) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + const char *names[] = { "h", "-help" }; + OakCommonCommandLineOption *hit = nullptr; + OakCommonCommandLineOption *miss = nullptr; + + ASSERT_EQ(oakcommon_commandlineparser_add_option( + parser, names, 2, "Show help", 0, nullptr, 0, &hit), + OAKCOMMON_OK); + ASSERT_NE(hit, nullptr); + + const char *other_names[] = { "v", "-version" }; + ASSERT_EQ(oakcommon_commandlineparser_add_option( + parser, other_names, 2, "Show version", 0, nullptr, 0, &miss), + OAKCOMMON_OK); + ASSERT_NE(miss, nullptr); + + const char *argv[] = { "oak", "-h" }; + ASSERT_EQ(oakcommon_commandlineparser_process(parser, argv, 2), + OAKCOMMON_OK); + + bool is_set = true; + ASSERT_EQ(oakcommon_commandlineoption_is_set(hit, &is_set), OAKCOMMON_OK); + EXPECT_TRUE(is_set); + + is_set = true; + ASSERT_EQ(oakcommon_commandlineoption_is_set(miss, &is_set), OAKCOMMON_OK); + EXPECT_FALSE(is_set); + + oakcommon_commandlineoption_free(hit); + oakcommon_commandlineoption_free(miss); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, ProcessMatchesSecondAliasCaseInsensitive) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + const char *names[] = { "h", "-help" }; + OakCommonCommandLineOption *option = nullptr; + ASSERT_EQ(oakcommon_commandlineparser_add_option( + parser, names, 2, "Show help", 0, nullptr, 0, &option), + OAKCOMMON_OK); + + // Only the first dash is stripped; matching is case-insensitive + const char *argv[] = { "oak", "--HELP" }; + ASSERT_EQ(oakcommon_commandlineparser_process(parser, argv, 2), + OAKCOMMON_OK); + + bool is_set = false; + ASSERT_EQ(oakcommon_commandlineoption_is_set(option, &is_set), + OAKCOMMON_OK); + EXPECT_TRUE(is_set); + + oakcommon_commandlineoption_free(option); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, OptionTakesArg) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + const char *names[] = { "e", "-export" }; + OakCommonCommandLineOption *option = nullptr; + ASSERT_EQ(oakcommon_commandlineparser_add_option( + parser, names, 2, "Export", 1, "filename", 0, &option), + OAKCOMMON_OK); + + const char *argv[] = { "oak", "-e", "/tmp/out.mp4" }; + ASSERT_EQ(oakcommon_commandlineparser_process(parser, argv, 3), + OAKCOMMON_OK); + + bool is_set = false; + ASSERT_EQ(oakcommon_commandlineoption_is_set(option, &is_set), + OAKCOMMON_OK); + EXPECT_TRUE(is_set); + + int required = + oakcommon_commandlineoption_get_setting(option, nullptr, 0); + ASSERT_GT(required, 0); + EXPECT_EQ(required, static_cast(strlen("/tmp/out.mp4")) + 1); + + std::vector buf(static_cast(required)); + ASSERT_EQ(oakcommon_commandlineoption_get_setting(option, buf.data(), + required), + required); + EXPECT_STREQ(buf.data(), "/tmp/out.mp4"); + + oakcommon_commandlineoption_free(option); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, PositionalArgument) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + OakCommonCommandLinePositionalArgument *arg = nullptr; + ASSERT_EQ(oakcommon_commandlineparser_add_positional_argument( + parser, "project", "Project file", 0, &arg), + OAKCOMMON_OK); + ASSERT_NE(arg, nullptr); + + const char *argv[] = { "oak", "/tmp/project.ove" }; + ASSERT_EQ(oakcommon_commandlineparser_process(parser, argv, 2), + OAKCOMMON_OK); + + int required = + oakcommon_commandlinepositionalargument_get_setting(arg, nullptr, 0); + ASSERT_EQ(required, static_cast(strlen("/tmp/project.ove")) + 1); + + std::vector buf(static_cast(required)); + ASSERT_EQ(oakcommon_commandlinepositionalargument_get_setting( + arg, buf.data(), required), + required); + EXPECT_STREQ(buf.data(), "/tmp/project.ove"); + + oakcommon_commandlinepositionalargument_free(arg); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, PositionalArgumentSetGetSetting) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + OakCommonCommandLinePositionalArgument *arg = nullptr; + ASSERT_EQ(oakcommon_commandlineparser_add_positional_argument( + parser, "project", "Project file", 0, &arg), + OAKCOMMON_OK); + ASSERT_NE(arg, nullptr); + + ASSERT_EQ(oakcommon_commandlinepositionalargument_set_setting(arg, + "hello.ove"), + OAKCOMMON_OK); + + char buf[64]; + int required = + oakcommon_commandlinepositionalargument_get_setting(arg, buf, + sizeof(buf)); + EXPECT_EQ(required, static_cast(strlen("hello.ove")) + 1); + EXPECT_STREQ(buf, "hello.ove"); + + // Error paths + EXPECT_EQ(oakcommon_commandlinepositionalargument_set_setting(nullptr, + "x"), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_commandlinepositionalargument_set_setting(arg, + nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_commandlinepositionalargument_get_setting(nullptr, + buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + + oakcommon_commandlinepositionalargument_free(arg); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, TwoStageStringGetterSmallBuffer) +{ + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + + const char *names[] = { "e" }; + OakCommonCommandLineOption *option = nullptr; + ASSERT_EQ(oakcommon_commandlineparser_add_option( + parser, names, 1, "Export", 1, "filename", 0, &option), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_commandlineoption_set_setting(option, "abcdef"), + OAKCOMMON_OK); + + // Buffer too small: return value reports the required size (7) and + // the written contents are truncated but NUL-terminated + char buf[4]; + int required = oakcommon_commandlineoption_get_setting(option, buf, + sizeof(buf)); + EXPECT_EQ(required, 7); + EXPECT_STREQ(buf, "abc"); + + // NULL buffer / zero size only reports the required size + EXPECT_EQ(oakcommon_commandlineoption_get_setting(option, nullptr, 0), 7); + + // Error paths + EXPECT_EQ(oakcommon_commandlineoption_get_setting(nullptr, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_commandlineoption_is_set(option, nullptr), + OAKCOMMON_E_INVALID); + bool dummy = false; + EXPECT_EQ(oakcommon_commandlineoption_is_set(nullptr, &dummy), + OAKCOMMON_E_INVALID); + + oakcommon_commandlineoption_free(option); + oakcommon_commandlineparser_free(parser); +} + +TEST(CommandLineParser, ProcessInvalidArgs) +{ + EXPECT_EQ(oakcommon_commandlineparser_process(nullptr, nullptr, 0), + OAKCOMMON_E_INVALID); + + OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser, nullptr); + EXPECT_EQ(oakcommon_commandlineparser_process(parser, nullptr, 1), + OAKCOMMON_E_INVALID); + oakcommon_commandlineparser_free(parser); +} diff --git a/src/common/tests/current_test.cpp b/src/common/tests/current_test.cpp new file mode 100644 index 000000000..bff59b32c --- /dev/null +++ b/src/common/tests/current_test.cpp @@ -0,0 +1,130 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include + +#include "common/current.h" + +TEST(OakCommonCurrent, InstanceIsSingleton) +{ + OakCommonCurrent *a = oakcommon_current_instance(); + OakCommonCurrent *b = oakcommon_current_instance(); + + ASSERT_NE(a, nullptr); + EXPECT_EQ(a, b); +} + +TEST(OakCommonCurrent, FreeNullIsNoOp) +{ + oakcommon_current_free(nullptr); + oakcommon_current_free(oakcommon_current_instance()); + SUCCEED(); +} + +TEST(OakCommonCurrent, VideoParamsSetGetRoundTrip) +{ + OakCommonCurrent *c = oakcommon_current_instance(); + int *params = static_cast(malloc(sizeof(int))); + ASSERT_NE(params, nullptr); + *params = 42; + + ASSERT_EQ(oakcommon_current_set_video_params(c, params, free), + OAKCOMMON_OK); + + void *out = nullptr; + ASSERT_EQ(oakcommon_current_get_video_params(c, &out), OAKCOMMON_OK); + EXPECT_EQ(out, params); + EXPECT_EQ(*static_cast(out), 42); + + // Clearing the slot must invoke the destroy callback. + ASSERT_EQ(oakcommon_current_set_video_params(c, nullptr, nullptr), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_current_get_video_params(c, &out), OAKCOMMON_OK); + EXPECT_EQ(out, nullptr); +} + +TEST(OakCommonCurrent, AudioParamsSetGetRoundTrip) +{ + OakCommonCurrent *c = oakcommon_current_instance(); + int value = 7; // non-owning storage, no destroy callback + + ASSERT_EQ(oakcommon_current_set_audio_params(c, &value, nullptr), + OAKCOMMON_OK); + + void *out = nullptr; + ASSERT_EQ(oakcommon_current_get_audio_params(c, &out), OAKCOMMON_OK); + EXPECT_EQ(out, &value); + + ASSERT_EQ(oakcommon_current_set_audio_params(c, nullptr, nullptr), + OAKCOMMON_OK); +} + +TEST(OakCommonCurrent, PluginHostAndCacheRoundTrip) +{ + OakCommonCurrent *c = oakcommon_current_instance(); + int host = 1, cache = 2; + + ASSERT_EQ(oakcommon_current_set_plugin_host(c, &host, nullptr), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_current_set_plugin_cache(c, &cache, nullptr), + OAKCOMMON_OK); + + void *out = nullptr; + ASSERT_EQ(oakcommon_current_get_plugin_host(c, &out), OAKCOMMON_OK); + EXPECT_EQ(out, &host); + ASSERT_EQ(oakcommon_current_get_plugin_cache(c, &out), OAKCOMMON_OK); + EXPECT_EQ(out, &cache); + + ASSERT_EQ(oakcommon_current_set_plugin_host(c, nullptr, nullptr), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_current_set_plugin_cache(c, nullptr, nullptr), + OAKCOMMON_OK); +} + +TEST(OakCommonCurrent, NullHandleAndOutArgs) +{ + void *out = nullptr; + int flag = 0; + + EXPECT_EQ(oakcommon_current_set_video_params(nullptr, &flag, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_current_get_video_params(nullptr, &out), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_current_get_video_params( + oakcommon_current_instance(), nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_current_is_interactive(nullptr, &flag), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_current_is_interactive( + oakcommon_current_instance(), nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonCurrent, IsInteractive) +{ + int flag = 0; + + ASSERT_EQ(oakcommon_current_is_interactive( + oakcommon_current_instance(), &flag), + OAKCOMMON_OK); + EXPECT_EQ(flag, 1); +} diff --git a/src/common/tests/debug_test.cpp b/src/common/tests/debug_test.cpp new file mode 100644 index 000000000..a824c3b23 --- /dev/null +++ b/src/common/tests/debug_test.cpp @@ -0,0 +1,69 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "common/debug.h" + +TEST(OakCommonDebug, LogValidMessage) +{ + EXPECT_EQ(oakcommon_debug_log(OAKCOMMON_DEBUG_WARNING, "hello"), + OAKCOMMON_OK); +} + +TEST(OakCommonDebug, LogNullMessage) +{ + EXPECT_EQ(oakcommon_debug_log(OAKCOMMON_DEBUG_WARNING, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonDebug, LogOutOfRangeLevel) +{ + // Out-of-range levels are tolerated and print as UNKNOWN. + EXPECT_EQ(oakcommon_debug_log(999, "odd level"), OAKCOMMON_OK); +} + +TEST(OakCommonDebug, LevelNameRoundTrip) +{ + char buf[16]; + + int needed = oakcommon_debug_level_name(OAKCOMMON_DEBUG_WARNING, + buf, sizeof(buf)); + ASSERT_GT(needed, 0); + ASSERT_LE(needed, (int)sizeof(buf)); + EXPECT_STREQ(buf, "WARNING"); +} + +TEST(OakCommonDebug, LevelNameQuerySize) +{ + int needed = oakcommon_debug_level_name(OAKCOMMON_DEBUG_DEBUG, + nullptr, 0); + EXPECT_EQ(needed, 6); // "DEBUG" + NUL +} + +TEST(OakCommonDebug, LevelNameUnknownLevel) +{ + char buf[16]; + + int needed = oakcommon_debug_level_name(-1, buf, sizeof(buf)); + ASSERT_GT(needed, 0); + ASSERT_LE(needed, (int)sizeof(buf)); + EXPECT_STREQ(buf, "UNKNOWN"); +} diff --git a/src/common/tests/dropworkflowbehavior_test.cpp b/src/common/tests/dropworkflowbehavior_test.cpp new file mode 100644 index 000000000..1e41a9edf --- /dev/null +++ b/src/common/tests/dropworkflowbehavior_test.cpp @@ -0,0 +1,76 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "common/dropworkflowbehavior.h" + +TEST(OakCommonDropWorkflowBehavior, IsValidAcceptsAllEnumerators) +{ + EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(OAKCOMMON_DWS_ASK), + 1); + EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(OAKCOMMON_DWS_AUTO), + 1); + EXPECT_EQ( + oakcommon_drop_workflow_behavior_is_valid(OAKCOMMON_DWS_MANUAL), + 1); + EXPECT_EQ( + oakcommon_drop_workflow_behavior_is_valid(OAKCOMMON_DWS_DISABLE), + 1); +} + +TEST(OakCommonDropWorkflowBehavior, IsValidRejectsOutOfRange) +{ + EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(-1), 0); + EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(4), 0); +} + +TEST(OakCommonDropWorkflowBehavior, NameRoundTrip) +{ + char buf[16]; + + int needed = oakcommon_drop_workflow_behavior_name(OAKCOMMON_DWS_AUTO, + buf, sizeof(buf)); + ASSERT_GT(needed, 0); + ASSERT_LE(needed, (int)sizeof(buf)); + EXPECT_STREQ(buf, "AUTO"); +} + +TEST(OakCommonDropWorkflowBehavior, NameQuerySizeAndTooSmallBuffer) +{ + int needed = oakcommon_drop_workflow_behavior_name(OAKCOMMON_DWS_DISABLE, + nullptr, 0); + EXPECT_EQ(needed, 8); // "DISABLE" + NUL + + char buf[4]; + EXPECT_EQ(oakcommon_drop_workflow_behavior_name(OAKCOMMON_DWS_DISABLE, + buf, sizeof(buf)), + needed); +} + +TEST(OakCommonDropWorkflowBehavior, NameInvalidValue) +{ + char buf[16]; + + int needed = oakcommon_drop_workflow_behavior_name(99, buf, sizeof(buf)); + ASSERT_GT(needed, 0); + ASSERT_LE(needed, (int)sizeof(buf)); + EXPECT_STREQ(buf, "UNKNOWN"); +} diff --git a/src/common/tests/ffmpegutils_test.cpp b/src/common/tests/ffmpegutils_test.cpp new file mode 100644 index 000000000..0a9de163b --- /dev/null +++ b/src/common/tests/ffmpegutils_test.cpp @@ -0,0 +1,206 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include + +#include "common/ffmpegutils.h" + +#include "../src/ffmpegutils.h" + +TEST(OakCommonFFmpegUtils, GetCompatiblePixelFormatMapsCorrectly) +{ + int out = -2; + + EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_pixel_format( + olive::core::PixelFormat::u8, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::PixelFormat::u8); + + EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_pixel_format( + olive::core::PixelFormat::u10, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::PixelFormat::u8); + + EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_pixel_format( + olive::core::PixelFormat::f32, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::PixelFormat::u16); + + EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_pixel_format( + olive::core::PixelFormat::invalid, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::PixelFormat::invalid); +} + +TEST(OakCommonFFmpegUtils, GetCompatiblePixelFormatNullOut) +{ + EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_pixel_format( + olive::core::PixelFormat::u8, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonFFmpegUtils, GetFFmpegPixelFormatMapsCorrectly) +{ + int out = -2; + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + olive::core::PixelFormat::u8, + OAKCOMMON_RGBA_CHANNEL_COUNT, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_rgba); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + olive::core::PixelFormat::u8, + OAKCOMMON_RGB_CHANNEL_COUNT, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_rg_b24); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + olive::core::PixelFormat::u16, + OAKCOMMON_RGBA_CHANNEL_COUNT, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_rgb_a64_le); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + olive::core::PixelFormat::invalid, + OAKCOMMON_RGBA_CHANNEL_COUNT, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_none); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + olive::core::PixelFormat::u8, 2, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_none); +} + +TEST(OakCommonFFmpegUtils, GetFFmpegPixelFormatNullOut) +{ + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + olive::core::PixelFormat::u8, + OAKCOMMON_RGBA_CHANNEL_COUNT, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonFFmpegUtils, GetNativeSampleFormatMapsCorrectly) +{ + int out = -2; + + EXPECT_EQ(oakcommon_ffmpegutils_get_native_sample_format( + fb_sample_fmt_u8, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::SampleFormat::u8); + + EXPECT_EQ(oakcommon_ffmpegutils_get_native_sample_format( + fb_sample_fmt_fltp, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::SampleFormat::f32_p); + + EXPECT_EQ(oakcommon_ffmpegutils_get_native_sample_format( + fb_sample_fmt_dblp, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::SampleFormat::f64_p); + + EXPECT_EQ(oakcommon_ffmpegutils_get_native_sample_format( + fb_sample_fmt_none, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, olive::core::SampleFormat::invalid); +} + +TEST(OakCommonFFmpegUtils, GetNativeSampleFormatNullOut) +{ + EXPECT_EQ(oakcommon_ffmpegutils_get_native_sample_format( + fb_sample_fmt_u8, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonFFmpegUtils, GetFFmpegSampleFormatMapsCorrectly) +{ + int out = -2; + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_sample_format( + olive::core::SampleFormat::u8, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_sample_fmt_u8); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_sample_format( + olive::core::SampleFormat::f32_p, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_sample_fmt_fltp); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_sample_format( + olive::core::SampleFormat::f64_p, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_sample_fmt_dblp); + + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_sample_format( + olive::core::SampleFormat::invalid, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_sample_fmt_none); +} + +TEST(OakCommonFFmpegUtils, GetFFmpegSampleFormatNullOut) +{ + EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_sample_format( + olive::core::SampleFormat::u8, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonFFmpegUtils, ConvertJpegSpaceToRegularSpaceMapsCorrectly) +{ + int out = -2; + + EXPECT_EQ(oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( + fb_pix_fmt_yuv_j420_p, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_yu_v420_p); + + EXPECT_EQ(oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( + fb_pix_fmt_yuv_j444_p, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_yu_v444_p); + + EXPECT_EQ(oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( + fb_pix_fmt_rgba, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, fb_pix_fmt_rgba); +} + +TEST(OakCommonFFmpegUtils, ConvertJpegSpaceToRegularSpaceNullOut) +{ + EXPECT_EQ(oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( + fb_pix_fmt_rgba, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonFFmpegUtils, GetCompatibleBridgePixelFormatNullOut) +{ + EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( + fb_pix_fmt_rgba, -1, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakCommonFFmpegUtils, GetCompatibleBridgePixelFormatMapsCorrectly) +{ + /* Calls fb_find_best_pix_fmt_of_list() in ffmpeg_bridge, which + * requires a working FFmpeg runtime environment. */ + GTEST_SKIP() << "requires FFmpeg runtime environment"; +} diff --git a/src/common/tests/filefunctions_test.cpp b/src/common/tests/filefunctions_test.cpp new file mode 100644 index 000000000..d7b01ba0c --- /dev/null +++ b/src/common/tests/filefunctions_test.cpp @@ -0,0 +1,421 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include +#include +#include + +#include "common/filefunctions.h" + +namespace fs = std::filesystem; + +class FileFunctionsTest : public ::testing::Test { +protected: + void SetUp() override + { + handle_ = oakcommon_filefunctions_init(); + ASSERT_NE(handle_, nullptr); + + temp_dir_ = fs::temp_directory_path() / + fs::path("oakcommon_filefunctions_test_" + + std::to_string(::getpid())); + fs::remove_all(temp_dir_); + fs::create_directories(temp_dir_); + } + + void TearDown() override + { + oakcommon_filefunctions_free(handle_); + handle_ = nullptr; + + std::error_code ec; + fs::remove_all(temp_dir_, ec); + } + + std::string write_file(const std::string &name, + const std::string &content) + { + fs::path p = temp_dir_ / name; + fs::create_directories(p.parent_path()); + std::ofstream f(p, std::ios::binary); + f << content; + return p.string(); + } + + /** + * @brief Two-stage string call helper: query size, then fetch + */ + template + std::string fetch_string(Fn &&fn, int *first_return) + { + int required = fn(nullptr, 0); + if (first_return != nullptr) { + *first_return = required; + } + EXPECT_GT(required, 0); + if (required <= 0) { + return std::string(); + } + + std::vector buf(required); + int second = fn(buf.data(), required); + EXPECT_EQ(second, required); + return std::string(buf.data()); + } + + OakCommonFileFunctions *handle_ = nullptr; + fs::path temp_dir_; +}; + +TEST_F(FileFunctionsTest, FreeNullIsNoOp) +{ + // Must not crash + oakcommon_filefunctions_free(nullptr); +} + +TEST_F(FileFunctionsTest, NullHandleReturnsInvalid) +{ + char buf[16]; + int out = 0; + EXPECT_EQ(oakcommon_filefunctions_get_configuration_location( + nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_application_path(nullptr, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_temp_file_path(nullptr, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_auto_recovery_root(nullptr, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_unique_file_identifier( + nullptr, "x", buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_ensure_filename_extension( + nullptr, "x", "y", buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_read_file_as_string(nullptr, "x", + buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_safe_temporary_filename( + nullptr, "x", buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_formatted_executable_for_platform( + nullptr, "x", buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid(nullptr, "x", 1, + &out), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_can_copy_directory_without_overwriting( + nullptr, "a", "b", &out), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_copy_directory(nullptr, "a", "b", 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_rename_file_allow_overwrite( + nullptr, "a", "b", &out), + OAKCOMMON_E_INVALID); +} + +TEST_F(FileFunctionsTest, NullArgumentsReturnInvalid) +{ + char buf[16]; + int out = 0; + EXPECT_EQ(oakcommon_filefunctions_read_file_as_string(handle_, nullptr, + buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid(handle_, nullptr, + 1, &out), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid(handle_, "x", 1, + nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_get_configuration_location( + handle_, nullptr, -1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_filefunctions_rename_file_allow_overwrite( + handle_, nullptr, "b", &out), + OAKCOMMON_E_INVALID); +} + +TEST_F(FileFunctionsTest, ReadFileAsStringRoundTrip) +{ + std::string content = "hello oak\nline two\n"; + std::string path = write_file("read.txt", content); + + std::string result = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_read_file_as_string( + handle_, path.c_str(), buf, size); + }, + nullptr); + EXPECT_EQ(result, content); + + // Buffer too small: required size is reported, nothing written + char tiny[2] = {'z', '\0'}; + int required = oakcommon_filefunctions_read_file_as_string( + handle_, path.c_str(), tiny, sizeof(tiny)); + EXPECT_EQ(required, static_cast(content.size()) + 1); +} + +TEST_F(FileFunctionsTest, ReadFileAsStringMissingFile) +{ + std::string path = (temp_dir_ / "no_such_file.txt").string(); + char buf[8] = {}; + int required = oakcommon_filefunctions_read_file_as_string( + handle_, path.c_str(), buf, sizeof(buf)); + // Missing file yields an empty string (required size 1) + EXPECT_EQ(required, 1); + EXPECT_STREQ(buf, ""); +} + +TEST_F(FileFunctionsTest, EnsureFilenameExtension) +{ + std::string result = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_ensure_filename_extension( + handle_, "project", "ove", buf, size); + }, + nullptr); + EXPECT_EQ(result, "project.ove"); + + // Already present (case-insensitive): unchanged + result = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_ensure_filename_extension( + handle_, "project.OVE", "ove", buf, size); + }, + nullptr); + EXPECT_EQ(result, "project.OVE"); +} + +TEST_F(FileFunctionsTest, GetUniqueFileIdentifierStable) +{ + std::string path = write_file("id.txt", "some data"); + + std::string id1 = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_unique_file_identifier( + handle_, path.c_str(), buf, size); + }, + nullptr); + EXPECT_FALSE(id1.empty()); + + std::string id2 = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_unique_file_identifier( + handle_, path.c_str(), buf, size); + }, + nullptr); + EXPECT_EQ(id1, id2); +} + +TEST_F(FileFunctionsTest, GetUniqueFileIdentifierMissingFile) +{ + std::string path = (temp_dir_ / "missing.bin").string(); + char buf[8] = {}; + int required = oakcommon_filefunctions_get_unique_file_identifier( + handle_, path.c_str(), buf, sizeof(buf)); + EXPECT_EQ(required, 1); + EXPECT_STREQ(buf, ""); +} + +TEST_F(FileFunctionsTest, DirectoryIsValidCreateAndCheck) +{ + int out = 0; + + // Existing directory + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid( + handle_, temp_dir_.string().c_str(), 0, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 1); + + // Non-existing directory, allowed to create + std::string sub = (temp_dir_ / "a" / "b").string(); + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid( + handle_, sub.c_str(), 1, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 1); + EXPECT_TRUE(fs::is_directory(sub)); + + // Non-existing directory, not allowed to create + std::string sub2 = (temp_dir_ / "c").string(); + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid( + handle_, sub2.c_str(), 0, &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 0); + EXPECT_FALSE(fs::exists(sub2)); +} + +TEST_F(FileFunctionsTest, GetSafeTemporaryFilenameDoesNotExist) +{ + std::string base = write_file("safe.ove", "data"); + + std::string temp = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_safe_temporary_filename( + handle_, base.c_str(), buf, size); + }, + nullptr); + EXPECT_FALSE(temp.empty()); + EXPECT_FALSE(fs::exists(temp)); + EXPECT_NE(temp, base); + EXPECT_TRUE(temp.find(".tmp") != std::string::npos); +} + +TEST_F(FileFunctionsTest, RenameFileAllowOverwrite) +{ + std::string from = write_file("from.txt", "new content"); + std::string to = write_file("to.txt", "old content"); + + int out = 0; + EXPECT_EQ(oakcommon_filefunctions_rename_file_allow_overwrite( + handle_, from.c_str(), to.c_str(), &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 1); + EXPECT_FALSE(fs::exists(from)); + + std::ifstream f(to, std::ios::binary); + std::string content((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + EXPECT_EQ(content, "new content"); +} + +TEST_F(FileFunctionsTest, RenameFileAllowOverwriteMissingSource) +{ + std::string from = (temp_dir_ / "no_source.txt").string(); + std::string to = (temp_dir_ / "target.txt").string(); + + int out = 1; + EXPECT_EQ(oakcommon_filefunctions_rename_file_allow_overwrite( + handle_, from.c_str(), to.c_str(), &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 0); +} + +TEST_F(FileFunctionsTest, CopyDirectoryAndOverwriteCheck) +{ + write_file("src/one.txt", "one"); + write_file("src/nested/two.txt", "two"); + std::string src = (temp_dir_ / "src").string(); + std::string dst = (temp_dir_ / "dst").string(); + + // Nothing at dest: safe to copy + int out = 0; + EXPECT_EQ(oakcommon_filefunctions_can_copy_directory_without_overwriting( + handle_, src.c_str(), dst.c_str(), &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 1); + + EXPECT_EQ(oakcommon_filefunctions_copy_directory(handle_, src.c_str(), + dst.c_str(), 0), + OAKCOMMON_OK); + EXPECT_TRUE(fs::exists(temp_dir_ / "dst" / "one.txt")); + EXPECT_TRUE(fs::exists(temp_dir_ / "dst" / "nested" / "two.txt")); + + // Files exist at dest now: no longer safe + EXPECT_EQ(oakcommon_filefunctions_can_copy_directory_without_overwriting( + handle_, src.c_str(), dst.c_str(), &out), + OAKCOMMON_OK); + EXPECT_EQ(out, 0); + + // Read back copied content through the C API + std::string copied = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_read_file_as_string( + handle_, + (temp_dir_ / "dst" / "nested" / "two.txt") + .string() + .c_str(), + buf, size); + }, + nullptr); + EXPECT_EQ(copied, "two"); +} + +TEST_F(FileFunctionsTest, CopyDirectoryMissingSource) +{ + std::string src = (temp_dir_ / "no_such_dir").string(); + std::string dst = (temp_dir_ / "dst2").string(); + + // Underlying call logs and returns without copying; must not crash + EXPECT_EQ(oakcommon_filefunctions_copy_directory(handle_, src.c_str(), + dst.c_str(), 0), + OAKCOMMON_OK); + EXPECT_FALSE(fs::exists(temp_dir_ / "dst2" / "anything")); +} + +TEST_F(FileFunctionsTest, LocationGettersReturnNonEmpty) +{ + int first_return = 0; + + std::string config = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_configuration_location( + handle_, buf, size); + }, + &first_return); + EXPECT_FALSE(config.empty()); + EXPECT_EQ(first_return, static_cast(config.size()) + 1); + + std::string temp = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_temp_file_path( + handle_, buf, size); + }, + nullptr); + EXPECT_FALSE(temp.empty()); + EXPECT_TRUE(fs::is_directory(temp)); + + std::string app = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_application_path( + handle_, buf, size); + }, + nullptr); + EXPECT_FALSE(app.empty()); + + std::string recovery = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_auto_recovery_root( + handle_, buf, size); + }, + nullptr); + EXPECT_FALSE(recovery.empty()); +} + +TEST_F(FileFunctionsTest, GetFormattedExecutableForPlatform) +{ + std::string result = fetch_string( + [&](char *buf, int size) { + return oakcommon_filefunctions_get_formatted_executable_for_platform( + handle_, "oak", buf, size); + }, + nullptr); +#ifdef _WIN32 + EXPECT_EQ(result, "oak.exe"); +#else + EXPECT_EQ(result, "oak"); +#endif +} diff --git a/src/common/tests/memorypool_test.cpp b/src/common/tests/memorypool_test.cpp new file mode 100644 index 000000000..1cca7b9f2 --- /dev/null +++ b/src/common/tests/memorypool_test.cpp @@ -0,0 +1,145 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include + +#include "../src/memorypool.h" + +namespace +{ + +/** + * @brief Test pool with a fixed 16-byte element size + */ +class TestPool : public olive::MemoryPool { +public: + TestPool(int element_count) : MemoryPool(element_count) + { + } + +protected: + size_t get_element_size() override + { + return 16; + } +}; + +} // namespace + +TEST(MemoryPool, AllocateReturnsWritableElement) +{ + TestPool pool(4); + + auto e = pool.get(); + ASSERT_NE(e, nullptr); + ASSERT_NE(e->data(), nullptr); + + // The full element must be writable + memset(e->data(), 0xAB, 16); + EXPECT_EQ(e->data()[0], 0xAB); + EXPECT_EQ(e->data()[15], 0xAB); + + EXPECT_TRUE(pool.is_allocated()); + EXPECT_EQ(pool.get_arena_count(), 1); +} + +TEST(MemoryPool, ReleasedElementIsReused) +{ + TestPool pool(4); + + uint8_t *first = nullptr; + { + auto e = pool.get(); + ASSERT_NE(e, nullptr); + first = e->data(); + } + + // After the ElementPtr went out of scope the chunk must be recycled + auto e2 = pool.get(); + ASSERT_NE(e2, nullptr); + EXPECT_EQ(e2->data(), first); + EXPECT_EQ(pool.get_arena_count(), 1); +} + +TEST(MemoryPool, FullArenaGrowsNewArena) +{ + TestPool pool(2); + + auto e1 = pool.get(); + auto e2 = pool.get(); + ASSERT_NE(e1, nullptr); + ASSERT_NE(e2, nullptr); + EXPECT_EQ(pool.get_arena_count(), 1); + + // Both slots lent out, next get() must allocate a second arena + auto e3 = pool.get(); + ASSERT_NE(e3, nullptr); + EXPECT_EQ(pool.get_arena_count(), 2); +} + +TEST(MemoryPool, ClearFreesAllArenas) +{ + TestPool pool(4); + + auto e = pool.get(); + ASSERT_NE(e, nullptr); + e->release(); + e.reset(); + + pool.clear(); + EXPECT_FALSE(pool.is_allocated()); + EXPECT_EQ(pool.get_arena_count(), 0); +} + +TEST(MemoryPool, InvalidElementCountReturnsNull) +{ + TestPool pool(0); + EXPECT_EQ(pool.get(), nullptr); + + TestPool negative(-1); + EXPECT_EQ(negative.get(), nullptr); +} + +TEST(MemoryPool, ClearEmptyArenasKeepsRecentlyEmptiedArena) +{ + TestPool pool(2); + + { + auto e = pool.get(); + ASSERT_NE(e, nullptr); + } + + // Arena was emptied just now, well within kMaxEmptyArenaLife, so it must survive + pool.clear_empty_arenas(); + EXPECT_EQ(pool.get_arena_count(), 1); +} + +TEST(MemoryPool, ClearEmptyArenasKeepsArenaInUse) +{ + TestPool pool(2); + + auto e = pool.get(); + ASSERT_NE(e, nullptr); + + pool.clear_empty_arenas(); + EXPECT_EQ(pool.get_arena_count(), 1); +} diff --git a/src/common/tests/miscutils_test.cpp b/src/common/tests/miscutils_test.cpp new file mode 100644 index 000000000..d9bbfdec6 --- /dev/null +++ b/src/common/tests/miscutils_test.cpp @@ -0,0 +1,164 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include + +#include "common/miscutils.h" + +TEST(MiscUtilsDecibel, FromLinearZero) +{ + double db; + ASSERT_EQ(oakcommon_decibel_from_linear(0.0, &db), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(db, OAKCOMMON_DECIBEL_MINIMUM); +} + +TEST(MiscUtilsDecibel, FromLinearNegative) +{ + double db; + ASSERT_EQ(oakcommon_decibel_from_linear(-1.0, &db), OAKCOMMON_OK); + // log10 of a negative value is NaN, not clamped to minimum + EXPECT_TRUE(std::isnan(db)); +} + +TEST(MiscUtilsDecibel, FromLinearOne) +{ + double db; + ASSERT_EQ(oakcommon_decibel_from_linear(1.0, &db), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(db, 0.0); +} + +TEST(MiscUtilsDecibel, FromLinearNullOut) +{ + EXPECT_EQ(oakcommon_decibel_from_linear(1.0, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(MiscUtilsDecibel, ToLinearZeroDb) +{ + double linear; + ASSERT_EQ(oakcommon_decibel_to_linear(0.0, &linear), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(linear, 1.0); +} + +TEST(MiscUtilsDecibel, ToLinearMinimum) +{ + double linear; + ASSERT_EQ(oakcommon_decibel_to_linear(OAKCOMMON_DECIBEL_MINIMUM, + &linear), + OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(linear, 0.0); +} + +TEST(MiscUtilsDecibel, ToLinearNullOut) +{ + EXPECT_EQ(oakcommon_decibel_to_linear(0.0, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(MiscUtilsDecibel, LinearRoundTrip) +{ + double db; + double linear; + ASSERT_EQ(oakcommon_decibel_from_linear(0.5, &db), OAKCOMMON_OK); + ASSERT_EQ(oakcommon_decibel_to_linear(db, &linear), OAKCOMMON_OK); + EXPECT_NEAR(linear, 0.5, 1e-12); +} + +TEST(MiscUtilsDecibel, FromLogarithmicBounds) +{ + double db; + ASSERT_EQ(oakcommon_decibel_from_logarithmic(0.0, &db), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(db, OAKCOMMON_DECIBEL_MINIMUM); + + ASSERT_EQ(oakcommon_decibel_from_logarithmic(1.0, &db), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(db, 0.0); +} + +TEST(MiscUtilsDecibel, FromLogarithmicNullOut) +{ + EXPECT_EQ(oakcommon_decibel_from_logarithmic(0.5, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(MiscUtilsDecibel, ToLogarithmicZeroDb) +{ + double logarithmic; + ASSERT_EQ(oakcommon_decibel_to_logarithmic(0.0, &logarithmic), + OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(logarithmic, 1.0); +} + +TEST(MiscUtilsDecibel, ToLogarithmicNullOut) +{ + EXPECT_EQ(oakcommon_decibel_to_logarithmic(0.0, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(MiscUtilsDecibel, LogarithmicRoundTrip) +{ + double logarithmic; + double linear; + ASSERT_EQ(oakcommon_decibel_linear_to_logarithmic(0.5, &logarithmic), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_decibel_logarithmic_to_linear(logarithmic, &linear), + OAKCOMMON_OK); + EXPECT_NEAR(linear, 0.5, 1e-12); +} + +TEST(MiscUtilsDecibel, LogarithmicDirectNullOut) +{ + double out; + EXPECT_EQ(oakcommon_decibel_linear_to_logarithmic(0.5, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_decibel_logarithmic_to_linear(0.5, nullptr), + OAKCOMMON_E_INVALID); + (void)out; +} + +TEST(MiscUtilsLerp, Endpoints) +{ + double value; + ASSERT_EQ(oakcommon_lerp(2.0, 8.0, 0.0, &value), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(value, 2.0); + + ASSERT_EQ(oakcommon_lerp(2.0, 8.0, 1.0, &value), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(value, 8.0); +} + +TEST(MiscUtilsLerp, Midpoint) +{ + double value; + ASSERT_EQ(oakcommon_lerp(2.0, 8.0, 0.5, &value), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(value, 5.0); +} + +TEST(MiscUtilsLerp, NegativeValues) +{ + double value; + ASSERT_EQ(oakcommon_lerp(-10.0, 10.0, 0.25, &value), OAKCOMMON_OK); + EXPECT_DOUBLE_EQ(value, -5.0); +} + +TEST(MiscUtilsLerp, NullOut) +{ + EXPECT_EQ(oakcommon_lerp(0.0, 1.0, 0.5, nullptr), OAKCOMMON_E_INVALID); +} diff --git a/src/common/tests/ocioutils_test.cpp b/src/common/tests/ocioutils_test.cpp new file mode 100644 index 000000000..bf204f230 --- /dev/null +++ b/src/common/tests/ocioutils_test.cpp @@ -0,0 +1,122 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +namespace ocio = OCIO_NAMESPACE; + +#include "common/ocioutils.h" + +/** + * @note The whole oakcommon ocioutils family links against OpenColorIO; + * if the test binary is built without OCIO these tests cannot run at + * all. The mapping itself is pure logic and needs no OCIO config or GPU, + * so no GTEST_SKIP() is required at runtime. + */ + +TEST(OCIOUtilsCApi, InitReturnsHandle) +{ + OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils, nullptr); + oakcommon_ocioutils_free(utils); +} + +TEST(OCIOUtilsCApi, FreeNullIsNoOp) +{ + oakcommon_ocioutils_free(NULL); +} + +TEST(OCIOUtilsCApi, BitDepthMappingMatchesOCIO) +{ + OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils, nullptr); + + const struct { + int pixel_format; + ocio::BitDepth expected; + } cases[] = { + { OAKCOMMON_PIXEL_FORMAT_U8, ocio::BIT_DEPTH_UINT8 }, + { OAKCOMMON_PIXEL_FORMAT_U10, ocio::BIT_DEPTH_UINT10 }, + { OAKCOMMON_PIXEL_FORMAT_U16, ocio::BIT_DEPTH_UINT16 }, + { OAKCOMMON_PIXEL_FORMAT_F16, ocio::BIT_DEPTH_F16 }, + { OAKCOMMON_PIXEL_FORMAT_F32, ocio::BIT_DEPTH_F32 }, + }; + + for (const auto &c : cases) { + int depth = -1; + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + utils, c.pixel_format, &depth), + OAKCOMMON_OK); + EXPECT_EQ(depth, static_cast(c.expected)); + } + + oakcommon_ocioutils_free(utils); +} + +TEST(OCIOUtilsCApi, InvalidFormatYieldsUnknownDepth) +{ + OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils, nullptr); + + int depth = -1; + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + utils, OAKCOMMON_PIXEL_FORMAT_INVALID, &depth), + OAKCOMMON_OK); + EXPECT_EQ(depth, static_cast(ocio::BIT_DEPTH_UNKNOWN)); + + oakcommon_ocioutils_free(utils); +} + +TEST(OCIOUtilsCApi, NullHandleReturnsInvalid) +{ + int depth = 0; + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + NULL, OAKCOMMON_PIXEL_FORMAT_U8, &depth), + OAKCOMMON_E_INVALID); +} + +TEST(OCIOUtilsCApi, NullOutParamReturnsInvalid) +{ + OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils, nullptr); + + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + utils, OAKCOMMON_PIXEL_FORMAT_U8, NULL), + OAKCOMMON_E_INVALID); + + oakcommon_ocioutils_free(utils); +} + +TEST(OCIOUtilsCApi, OutOfRangeFormatReturnsInvalid) +{ + OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils, nullptr); + + int depth = 0; + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + utils, OAKCOMMON_PIXEL_FORMAT_COUNT, &depth), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( + utils, -2, &depth), + OAKCOMMON_E_INVALID); + + oakcommon_ocioutils_free(utils); +} diff --git a/src/common/tests/oiioutils_test.cpp b/src/common/tests/oiioutils_test.cpp new file mode 100644 index 000000000..ac0678af3 --- /dev/null +++ b/src/common/tests/oiioutils_test.cpp @@ -0,0 +1,191 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "common/oiioutils.h" + +/** + * @note The whole oakcommon oiioutils family links against OpenImageIO, + * but every function exposed through the C API is pure logic (enum + * mapping and double-to-rational conversion) that needs no image I/O, + * codec or GPU at runtime, so no GTEST_SKIP() is required. The + * OIIO::ImageBuf-based helpers (frame_to_buffer / buffer_to_frame) are + * intentionally not part of the C API and are not tested here. + * + * OIIO TypeDesc::BASETYPE values used below: UNKNOWN = 0, UINT8 = 2, + * UINT16 = 4, HALF = 10, FLOAT = 11, DOUBLE = 12 (LASTBASE is + * version-dependent; 100 is safely out of range). + */ + +TEST(OIIOUtilsCApi, InitReturnsHandle) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + oakcommon_oiioutils_free(utils); +} + +TEST(OIIOUtilsCApi, FreeNullIsNoOp) +{ + oakcommon_oiioutils_free(NULL); +} + +TEST(OIIOUtilsCApi, BaseTypeFromPixelFormat) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + + const struct { + int pixel_format; + int expected_base_type; + } cases[] = { + { OAKCOMMON_PIXEL_FORMAT_U8, 2 }, /* UINT8 */ + { OAKCOMMON_PIXEL_FORMAT_U16, 4 }, /* UINT16 */ + { OAKCOMMON_PIXEL_FORMAT_F16, 10 }, /* HALF */ + { OAKCOMMON_PIXEL_FORMAT_F32, 11 }, /* FLOAT */ + { OAKCOMMON_PIXEL_FORMAT_U10, 0 }, /* UNKNOWN: unmappable */ + { OAKCOMMON_PIXEL_FORMAT_INVALID, 0 }, /* UNKNOWN */ + }; + + for (const auto &c : cases) { + int base_type = -1; + EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( + utils, c.pixel_format, &base_type), + OAKCOMMON_OK); + EXPECT_EQ(base_type, c.expected_base_type); + } + + oakcommon_oiioutils_free(utils); +} + +TEST(OIIOUtilsCApi, BaseTypeFromPixelFormatRejectsBadArgs) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + + int base_type = 0; + EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( + NULL, OAKCOMMON_PIXEL_FORMAT_U8, &base_type), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( + utils, OAKCOMMON_PIXEL_FORMAT_U8, NULL), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( + utils, OAKCOMMON_PIXEL_FORMAT_COUNT, &base_type), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( + utils, -2, &base_type), + OAKCOMMON_E_INVALID); + + oakcommon_oiioutils_free(utils); +} + +TEST(OIIOUtilsCApi, PixelFormatFromBaseType) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + + const struct { + int base_type; + int expected_pixel_format; + } cases[] = { + { 2, OAKCOMMON_PIXEL_FORMAT_U8 }, /* UINT8 */ + { 4, OAKCOMMON_PIXEL_FORMAT_U16 }, /* UINT16 */ + { 10, OAKCOMMON_PIXEL_FORMAT_F16 }, /* HALF */ + { 11, OAKCOMMON_PIXEL_FORMAT_F32 }, /* FLOAT */ + { 0, OAKCOMMON_PIXEL_FORMAT_INVALID }, /* UNKNOWN */ + { 12, OAKCOMMON_PIXEL_FORMAT_INVALID }, /* DOUBLE: unmappable */ + { 13, OAKCOMMON_PIXEL_FORMAT_INVALID }, /* STRING: unmappable */ + }; + + for (const auto &c : cases) { + int pixel_format = -2; + EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( + utils, c.base_type, &pixel_format), + OAKCOMMON_OK); + EXPECT_EQ(pixel_format, c.expected_pixel_format); + } + + oakcommon_oiioutils_free(utils); +} + +TEST(OIIOUtilsCApi, PixelFormatFromBaseTypeRejectsBadArgs) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + + int pixel_format = 0; + EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( + NULL, 2, &pixel_format), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( + utils, 2, NULL), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( + utils, -1, &pixel_format), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( + utils, 100, &pixel_format), + OAKCOMMON_E_INVALID); /* >= LASTBASE */ + + oakcommon_oiioutils_free(utils); +} + +TEST(OIIOUtilsCApi, PixelAspectRatioConvertsToRational) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + + int num = 0; + int den = 0; + ASSERT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(utils, 1.0, &num, + &den), + OAKCOMMON_OK); + EXPECT_EQ(num, 1); + EXPECT_EQ(den, 1); + + ASSERT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(utils, 1.5, &num, + &den), + OAKCOMMON_OK); + ASSERT_NE(den, 0); + EXPECT_NEAR(static_cast(num) / den, 1.5, 1e-9); + + oakcommon_oiioutils_free(utils); +} + +TEST(OIIOUtilsCApi, PixelAspectRatioRejectsBadArgs) +{ + OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils, nullptr); + + int num = 0; + int den = 0; + EXPECT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(NULL, 1.0, &num, + &den), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(utils, 1.0, NULL, + &den), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(utils, 1.0, &num, + NULL), + OAKCOMMON_E_INVALID); + + oakcommon_oiioutils_free(utils); +} diff --git a/src/common/tests/power_test.cpp b/src/common/tests/power_test.cpp new file mode 100644 index 000000000..c71e2092e --- /dev/null +++ b/src/common/tests/power_test.cpp @@ -0,0 +1,69 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "../../include/common/power.h" + +TEST(Power, CeilToPowerOf2) +{ + uint32_t out = 0; + + ASSERT_EQ(oakcommon_power_ceil_to_power_of_2(1, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 1u); + + ASSERT_EQ(oakcommon_power_ceil_to_power_of_2(2, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 2u); + + ASSERT_EQ(oakcommon_power_ceil_to_power_of_2(3, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 4u); + + ASSERT_EQ(oakcommon_power_ceil_to_power_of_2(1000, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 1024u); +} + +TEST(Power, CeilToPowerOf2NullOut) +{ + EXPECT_EQ(oakcommon_power_ceil_to_power_of_2(3, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(Power, FloorToPowerOf2) +{ + uint32_t out = 0; + + ASSERT_EQ(oakcommon_power_floor_to_power_of_2(1, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 1u); + + ASSERT_EQ(oakcommon_power_floor_to_power_of_2(3, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 2u); + + ASSERT_EQ(oakcommon_power_floor_to_power_of_2(1024, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 1024u); + + ASSERT_EQ(oakcommon_power_floor_to_power_of_2(1025, &out), OAKCOMMON_OK); + EXPECT_EQ(out, 1024u); +} + +TEST(Power, FloorToPowerOf2NullOut) +{ + EXPECT_EQ(oakcommon_power_floor_to_power_of_2(3, nullptr), + OAKCOMMON_E_INVALID); +} diff --git a/src/common/tests/qtutils_test.cpp b/src/common/tests/qtutils_test.cpp new file mode 100644 index 000000000..f4687ea49 --- /dev/null +++ b/src/common/tests/qtutils_test.cpp @@ -0,0 +1,98 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include +#include + +#include + +#include "common/qtutils.h" + +TEST(QtUtils, PtrToValueRoundTrip) +{ + int value = 42; + uint64_t encoded = 0; + + ASSERT_EQ(oakcommon_qtutils_ptr_to_value(&value, &encoded), + OAKCOMMON_OK); + EXPECT_EQ(encoded, static_cast( + reinterpret_cast(&value))); + + void *decoded = nullptr; + ASSERT_EQ(oakcommon_qtutils_value_to_ptr(encoded, &decoded), + OAKCOMMON_OK); + EXPECT_EQ(decoded, static_cast(&value)); +} + +TEST(QtUtils, PtrToValueNullOutParam) +{ + int value = 42; + + EXPECT_EQ(oakcommon_qtutils_ptr_to_value(&value, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(QtUtils, ValueToPtrNullPointer) +{ + void *decoded = reinterpret_cast(0xdeadbeef); + + ASSERT_EQ(oakcommon_qtutils_value_to_ptr(0, &decoded), OAKCOMMON_OK); + EXPECT_EQ(decoded, nullptr); +} + +TEST(QtUtils, ValueToPtrNullOutParam) +{ + EXPECT_EQ(oakcommon_qtutils_value_to_ptr(0, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(QtUtils, GetCreationDateExistingFile) +{ + char path[] = "/tmp/oakcommon_qtutils_test_XXXXXX"; + int fd = mkstemp(path); + ASSERT_NE(fd, -1); + close(fd); + + int64_t secs = 0; + ASSERT_EQ(oakcommon_qtutils_get_creation_date(path, &secs), + OAKCOMMON_OK); + EXPECT_GT(secs, 0); + + std::remove(path); +} + +TEST(QtUtils, GetCreationDateMissingFile) +{ + int64_t secs = 0; + + EXPECT_EQ(oakcommon_qtutils_get_creation_date( + "/tmp/oakcommon_qtutils_no_such_file_12345", &secs), + OAKCOMMON_E_NOT_FOUND); +} + +TEST(QtUtils, GetCreationDateNullArgs) +{ + int64_t secs = 0; + + EXPECT_EQ(oakcommon_qtutils_get_creation_date(nullptr, &secs), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_qtutils_get_creation_date("/tmp", nullptr), + OAKCOMMON_E_INVALID); +} diff --git a/src/common/tests/threadsafemap_test.cpp b/src/common/tests/threadsafemap_test.cpp new file mode 100644 index 000000000..19665e4c5 --- /dev/null +++ b/src/common/tests/threadsafemap_test.cpp @@ -0,0 +1,89 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include + +#include "../src/threadsafemap.h" + +TEST(ThreadSafeMap, InsertAndGet) +{ + ThreadSafeMap map; + + map.insert(1, 42); + + int value = 0; + ASSERT_TRUE(map.get(1, &value)); + EXPECT_EQ(value, 42); + EXPECT_TRUE(map.contains(1)); + EXPECT_EQ(map.size(), 1u); +} + +TEST(ThreadSafeMap, InsertOverwritesExistingKey) +{ + ThreadSafeMap map; + + map.insert(1, 42); + map.insert(1, 7); + + int value = 0; + ASSERT_TRUE(map.get(1, &value)); + EXPECT_EQ(value, 7); + EXPECT_EQ(map.size(), 1u); +} + +TEST(ThreadSafeMap, MissingKeyReturnsFalse) +{ + ThreadSafeMap map; + + EXPECT_FALSE(map.contains(99)); + + int value = 1234; + EXPECT_FALSE(map.get(99, &value)); + // `out` must be left untouched on failure + EXPECT_EQ(value, 1234); +} + +TEST(ThreadSafeMap, ConcurrentInserts) +{ + ThreadSafeMap map; + const int k_threads = 8; + const int k_inserts_per_thread = 500; + + std::vector threads; + for (int t = 0; t < k_threads; t++) { + threads.emplace_back([&map, t, k_inserts_per_thread]() { + for (int i = 0; i < k_inserts_per_thread; i++) { + map.insert(t * k_inserts_per_thread + i, i); + } + }); + } + for (std::thread &th : threads) { + th.join(); + } + + EXPECT_EQ(map.size(), size_t(k_threads * k_inserts_per_thread)); + + int value = 0; + ASSERT_TRUE(map.get(3 * k_inserts_per_thread + 123, &value)); + EXPECT_EQ(value, 123); +} diff --git a/src/common/tests/xmlutils_test.cpp b/src/common/tests/xmlutils_test.cpp new file mode 100644 index 000000000..42be51d26 --- /dev/null +++ b/src/common/tests/xmlutils_test.cpp @@ -0,0 +1,330 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include + +#include "common/xmlutils.h" + +namespace +{ + +std::string read_string(int (*fn)(OakCommonXmlReader *, char *, int), + OakCommonXmlReader *reader) +{ + int needed = fn(reader, nullptr, 0); + EXPECT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(fn(reader, buf.data(), needed), needed); + return std::string(buf.data()); +} + +} // namespace + +TEST(CommonXmlUtilsCApi, ReaderInitNullData) +{ + EXPECT_EQ(oakcommon_xml_reader_init(nullptr), nullptr); +} + +TEST(CommonXmlUtilsCApi, ReaderFreeNull) +{ + oakcommon_xml_reader_free(nullptr); +} + +TEST(CommonXmlUtilsCApi, ReadNextStartElement) +{ + OakCommonXmlReader *r = + oakcommon_xml_reader_init("value"); + ASSERT_NE(r, nullptr); + + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "root"); + + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "child"); + + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, ReadNextStartElementNullHandle) +{ + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(nullptr, &found), + OAKCOMMON_E_INVALID); + + OakCommonXmlReader *r = oakcommon_xml_reader_init(""); + ASSERT_NE(r, nullptr); + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, nullptr), + OAKCOMMON_E_INVALID); + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, ReadNextStartElementReturnsFalseAtEnd) +{ + OakCommonXmlReader *r = oakcommon_xml_reader_init(""); + ASSERT_NE(r, nullptr); + + int found = -1; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 0); + + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, NameNullHandle) +{ + char buf[16]; + EXPECT_EQ(oakcommon_xml_reader_name(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); +} + +TEST(CommonXmlUtilsCApi, ReadElementText) +{ + OakCommonXmlReader *r = + oakcommon_xml_reader_init("a & b"); + ASSERT_NE(r, nullptr); + + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_read_element_text, r), + "a & b"); + + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, ReadElementTextNullHandle) +{ + char buf[16]; + EXPECT_EQ(oakcommon_xml_reader_read_element_text(nullptr, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); +} + +TEST(CommonXmlUtilsCApi, SkipCurrentElement) +{ + OakCommonXmlReader *r = oakcommon_xml_reader_init( + ""); + ASSERT_NE(r, nullptr); + + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "unknown"); + + EXPECT_EQ(oakcommon_xml_reader_skip_current_element(r), OAKCOMMON_OK); + + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "known"); + + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, SkipCurrentElementNullHandle) +{ + EXPECT_EQ(oakcommon_xml_reader_skip_current_element(nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(CommonXmlUtilsCApi, Attributes) +{ + OakCommonXmlReader *r = oakcommon_xml_reader_init( + ""); + ASSERT_NE(r, nullptr); + + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + + int count = 0; + EXPECT_EQ(oakcommon_xml_reader_attribute_count(r, &count), OAKCOMMON_OK); + ASSERT_EQ(count, 2); + + char buf[32]; + EXPECT_GT(oakcommon_xml_reader_attribute_name(r, 0, buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "id"); + EXPECT_GT(oakcommon_xml_reader_attribute_value(r, 0, buf, sizeof(buf)), + 0); + EXPECT_STREQ(buf, "7"); + EXPECT_GT(oakcommon_xml_reader_attribute_name(r, 1, buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "name"); + EXPECT_GT(oakcommon_xml_reader_attribute_value(r, 1, buf, sizeof(buf)), + 0); + EXPECT_STREQ(buf, "a\"b"); + + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, AttributeErrorPaths) +{ + EXPECT_EQ(oakcommon_xml_reader_attribute_count(nullptr, nullptr), + OAKCOMMON_E_INVALID); + + OakCommonXmlReader *r = oakcommon_xml_reader_init(""); + ASSERT_NE(r, nullptr); + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + + char buf[16]; + EXPECT_EQ(oakcommon_xml_reader_attribute_name(r, 5, buf, sizeof(buf)), + OAKCOMMON_E_NOT_FOUND); + EXPECT_EQ(oakcommon_xml_reader_attribute_value(r, -1, buf, sizeof(buf)), + OAKCOMMON_E_NOT_FOUND); + EXPECT_EQ(oakcommon_xml_reader_attribute_name(nullptr, 0, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + + oakcommon_xml_reader_free(r); +} + +TEST(CommonXmlUtilsCApi, HasError) +{ + OakCommonXmlReader *bad = + oakcommon_xml_reader_init(""); + ASSERT_NE(bad, nullptr); + int has_error = 0; + EXPECT_EQ(oakcommon_xml_reader_has_error(bad, &has_error), OAKCOMMON_OK); + EXPECT_EQ(has_error, 1); + oakcommon_xml_reader_free(bad); + + OakCommonXmlReader *good = oakcommon_xml_reader_init(""); + ASSERT_NE(good, nullptr); + EXPECT_EQ(oakcommon_xml_reader_has_error(good, &has_error), OAKCOMMON_OK); + EXPECT_EQ(has_error, 0); + EXPECT_EQ(oakcommon_xml_reader_has_error(nullptr, &has_error), + OAKCOMMON_E_INVALID); + oakcommon_xml_reader_free(good); +} + +TEST(CommonXmlUtilsCApi, WriterFreeNull) +{ + oakcommon_xml_writer_free(nullptr); +} + +TEST(CommonXmlUtilsCApi, WriterNullHandleAndArgs) +{ + char buf[16]; + EXPECT_EQ(oakcommon_xml_writer_write_start_element(nullptr, "a"), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_xml_writer_write_end_element(nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_xml_writer_write_end_document(nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_xml_writer_output(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + + OakCommonXmlWriter *w = oakcommon_xml_writer_init(); + ASSERT_NE(w, nullptr); + EXPECT_EQ(oakcommon_xml_writer_write_start_element(w, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_xml_writer_write_attribute(w, "a", nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_xml_writer_write_characters(w, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_xml_writer_write_text_element(w, nullptr, "x"), + OAKCOMMON_E_INVALID); + oakcommon_xml_writer_free(w); +} + +TEST(CommonXmlUtilsCApi, WriterRoundTrip) +{ + OakCommonXmlWriter *w = oakcommon_xml_writer_init(); + ASSERT_NE(w, nullptr); + + EXPECT_EQ(oakcommon_xml_writer_write_start_element(w, "root"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_writer_write_attribute(w, "version", "1"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_writer_write_text_element(w, "child", + "a & b"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_writer_write_start_element(w, "empty"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_writer_write_end_element(w), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_writer_write_end_document(w), OAKCOMMON_OK); + + int needed = oakcommon_xml_writer_output(w, nullptr, 0); + ASSERT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(oakcommon_xml_writer_output(w, buf.data(), needed), needed); + oakcommon_xml_writer_free(w); + + // Read the produced document back. + OakCommonXmlReader *r = oakcommon_xml_reader_init(buf.data()); + ASSERT_NE(r, nullptr); + int has_error = 1; + EXPECT_EQ(oakcommon_xml_reader_has_error(r, &has_error), OAKCOMMON_OK); + EXPECT_EQ(has_error, 0); + + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "root"); + + int count = 0; + EXPECT_EQ(oakcommon_xml_reader_attribute_count(r, &count), OAKCOMMON_OK); + ASSERT_EQ(count, 1); + char abuf[32]; + EXPECT_GT(oakcommon_xml_reader_attribute_value(r, 0, abuf, sizeof(abuf)), + 0); + EXPECT_STREQ(abuf, "1"); + + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "child"); + EXPECT_EQ(read_string(oakcommon_xml_reader_read_element_text, r), + "a & b"); + + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "empty"); + + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 0); + + oakcommon_xml_reader_free(r); +}