diff --git a/.devcontainer/all/Dockerfile b/.devcontainer/all/Dockerfile index 1d0096e..af4b8e7 100644 --- a/.devcontainer/all/Dockerfile +++ b/.devcontainer/all/Dockerfile @@ -37,6 +37,12 @@ RUN cd ~ && git clone --depth 1 'https://git.ffmpeg.org/ffmpeg.git' && cd ffmpeg && ./configure --enable-shared --disable-static --enable-gpl --enable-version3 --enable-libx264 --prefix=/usr \ && make -j$(grep -c ^processor /proc/cpuinfo) && make install \ && cd ~ && rm -rf ffmpeg +RUN cd ~ && git clone --depth 1 'https://github.com/Tencent/rapidjson' && cd rapidjson \ + && mkdir -p build && cd build \ + && cmake -DCMAKE_BUILD_TYPE=Release .. "-DCMAKE_INSTALL_PREFIX=/usr" -DRAPIDJSON_BUILD_DOC=OFF \ + -DRAPIDJSON_BUILD_EXAMPLES=OFF -DRAPIDJSON_BUILD_TESTS=OFF \ + && make -j$(grep -c ^processor /proc/cpuinfo) && make install \ + && cd ~ && rm -rf rapidjson RUN curl https://sh.rustup.rs -sSf | \ sh -s -- --default-toolchain nightly -y ENV PATH=/root/.cargo/bin:$PATH diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f1f24b5..9f081d1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -74,7 +74,7 @@ jobs: id: cache_key run: | cd scripts - python3 get_cache_key.py libzip x264 ffmpeg || exit 1 + python3 get_cache_key.py libzip x264 ffmpeg rapidjson || exit 1 - name: Cache id: cache uses: actions/cache@v4 @@ -92,6 +92,7 @@ jobs: ./build_libzip.sh || exit 1 ./build_x264.sh || exit 1 ./build_ffmpeg.sh || exit 1 + ./build_rapidjson.sh || exit 1 - name: Build run: | export PKG_CONFIG_PATH=`pwd`/clib/lib/pkgconfig @@ -136,7 +137,7 @@ jobs: id: cache_key run: | cd scripts - python3 get_cache_key.py exiv2 libzip x264 ffmpeg || exit 1 + python3 get_cache_key.py exiv2 libzip x264 ffmpeg rapidjson || exit 1 - name: Cache id: cache uses: actions/cache@v4 @@ -155,6 +156,7 @@ jobs: ./build_libzip.sh || exit 1 ./build_x264.sh || exit 1 ./build_ffmpeg.sh || exit 1 + ./build_rapidjson.sh || exit 1 - name: Build run: | export PKG_CONFIG_PATH=`pwd`/clib/lib/pkgconfig @@ -184,7 +186,7 @@ jobs: id: cache_key run: | cd scripts - python get_cache_key.py --prefix=win zlib pkgconf expat exiv2 openssl libzip x264 ffmpeg || exit 1 + python get_cache_key.py --prefix=win zlib pkgconf expat exiv2 openssl libzip x264 ffmpeg rapidjson || exit 1 - name: Cache id: cache uses: actions/cache@v4 @@ -257,6 +259,11 @@ jobs: run: | cp scripts/build_win_ffmpeg.sh -v ./ || exit 1 ./build_win_ffmpeg.sh || exit 1 + - name: Build rapidjson + if: steps.cache.outputs.cache-hit != 'true' + run: | + COPY /Y scripts\build_win_rapidjson.bat || exit 1 + CALL build_win_rapidjson.bat || exit 1 - name: Download certs run: | COPY /Y scripts\download_certs.bat || exit 1 @@ -279,8 +286,8 @@ jobs: run: | SET PATH=%CD%\clib\bin;%PATH% COPY /Y clib\ssl\cert.pem cert.pem - python scripts\pack_prog.py -o pixiv_downloader.7z -a cert.pem %CD%/target/release-with-debug/pixiv_downloader.exe || exit 1 - python scripts\pack_prog.py -o pixiv_downloader.pdb.7z -p %CD%/target/release-with-debug/pixiv_downloader.exe || exit 1 + python scripts\pack_prog.py -o pixiv_downloader.7z -a cert.pem %CD%/target/release-with-debug/pixiv_downloader.exe %CD%/target/release-with-debug/ugoira.exe || exit 1 + python scripts\pack_prog.py -o pixiv_downloader.pdb.7z -p %CD%/target/release-with-debug/pixiv_downloader.exe %CD%/target/release-with-debug/ugoira.exe || exit 1 - name: Upload files continue-on-error: true uses: actions/upload-artifact@v4 diff --git a/.github/workflows/github-pages.yaml b/.github/workflows/github-pages.yaml index e5868b5..f0136d3 100644 --- a/.github/workflows/github-pages.yaml +++ b/.github/workflows/github-pages.yaml @@ -58,7 +58,7 @@ jobs: id: cache_key run: | cd scripts - python3 get_cache_key.py exiv2 libzip x264 ffmpeg || exit 1 + python3 get_cache_key.py exiv2 libzip x264 ffmpeg rapidjson || exit 1 - name: Cache id: cache uses: actions/cache@v4 @@ -77,6 +77,7 @@ jobs: ./build_libzip.sh || exit 1 ./build_x264.sh || exit 1 ./build_ffmpeg.sh || exit 1 + ./build_rapidjson.sh || exit 1 - name: Document run: | export PKG_CONFIG_PATH=`pwd`/clib/lib/pkgconfig diff --git a/.gitmodules b/.gitmodules index 2676665..adb8040 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "utils"] path = utils url = https://github.com/lifegpc/c-utils +[submodule "getopt"] + path = getopt + url = https://github.com/lifegpc/getopt-MSVC diff --git a/Dockerfile b/Dockerfile index b6ecee7..e5c61b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,6 +65,12 @@ RUN cd ~ && \ -DCURL_USE_LIBSSH2=OFF -DCMAKE_INSTALL_PREFIX=/clib -DBUILD_TESTING=OFF ../ && \ make -j$(grep -c ^processor /proc/cpuinfo) && make install && \ cd ~ && rm -rf curl-8.8.0 curl-8.8.0.tar.gz +RUN cd ~ && git clone --depth 1 'https://github.com/Tencent/rapidjson' && cd rapidjson \ + && mkdir -p build && cd build \ + && cmake -DCMAKE_BUILD_TYPE=Release .. "-DCMAKE_INSTALL_PREFIX=/clib" -DRAPIDJSON_BUILD_DOC=OFF \ + -DRAPIDJSON_BUILD_EXAMPLES=OFF -DRAPIDJSON_BUILD_TESTS=OFF \ + && make -j$(grep -c ^processor /proc/cpuinfo) && make install \ + && cd ~ && rm -rf rapidjson WORKDIR /app COPY . /app RUN export PKG_CONFIG_PATH=/clib/lib/pkgconfig \ @@ -86,10 +92,11 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/target/release/pixiv_downloader /app/pixiv_downloader COPY --from=builder /clib/lib /app/lib COPY --from=builder /app/i18n-output /app COPY --from=builder /clib/bin /app/bin +COPY --from=builder /app/target/release/pixiv_downloader /app/pixiv_downloader +COPY --from=builder /app/target/release/ugoira /app/ugoira ENV LD_LIBRARY_PATH=/app/lib ENV PATH=/app/bin:$PATH ENV LC_ALL=C.utf8 diff --git a/build.rs b/build.rs index c98ee79..81f5f24 100644 --- a/build.rs +++ b/build.rs @@ -173,11 +173,13 @@ fn main() { if !ugoira_build_path.exists() { create_dir(&ugoira_build_path).unwrap(); } + let install_bin_dir = out_path.join("../../../"); let mut config = cmake::Config::new("ugoira"); config .define("CMAKE_INSTALL_PREFIX", out_path.to_str().unwrap()) .out_dir(ugoira_build_path) - .define("UTILS_LIBRARY", utils_build_path.to_str().unwrap()); + .define("UTILS_LIBRARY", utils_build_path.to_str().unwrap()) + .define("CMAKE_INSTALL_BINDIR", install_bin_dir.to_str().unwrap()); #[cfg(all(windows, target_env = "msvc"))] config .define("CMAKE_BUILD_TYPE", "Release") diff --git a/getopt b/getopt new file mode 160000 index 0000000..a16fe1f --- /dev/null +++ b/getopt @@ -0,0 +1 @@ +Subproject commit a16fe1fe76d933ca6987ba5af156678437b3d9a6 diff --git a/scripts/build_rapidjson.sh b/scripts/build_rapidjson.sh new file mode 100755 index 0000000..565c913 --- /dev/null +++ b/scripts/build_rapidjson.sh @@ -0,0 +1,6 @@ +export PREFIX=`pwd`/clib +mkdir -p cbuild && cd cbuild || exit 1 +git clone --depth 1 'https://github.com/Tencent/rapidjson' && cd rapidjson || exit 1 +mkdir -p build && cd build || exit 1 +cmake -DCMAKE_BUILD_TYPE=Release .. "-DCMAKE_INSTALL_PREFIX=$PREFIX" -DRAPIDJSON_BUILD_DOC=OFF -DRAPIDJSON_BUILD_EXAMPLES=OFF -DRAPIDJSON_BUILD_TESTS=OFF || exit 1 +make -j8 && make install || exit 1 diff --git a/scripts/build_win_rapidjson.bat b/scripts/build_win_rapidjson.bat new file mode 100644 index 0000000..16bc6a0 --- /dev/null +++ b/scripts/build_win_rapidjson.bat @@ -0,0 +1,25 @@ +@ECHO OFF +SETLOCAL +SET PREFIX=%CD%\clib +SET PKG_CONFIG_DIR=%PREFIX%\lib\pkgconfig +IF NOT EXIST cbuild ( + MD cbuild || EXIT /B 1 +) +CD cbuild || EXIT /B 1 +git clone --depth 1 "https://github.com/Tencent/rapidjson" || EXIT /B %ERRORLEVEL% +CD rapidjson || EXIT /B 1 +IF NOT EXIST build ( + MD build || EXIT /B 1 +) +CD build || EXIT /B 1 +cmake ^ + -G Ninja ^ + -DCMAKE_PREFIX_PATH=%PREFIX% ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_INSTALL_PREFIX=%PREFIX% ^ + -DRAPIDJSON_BUILD_DOC=OFF ^ + -DRAPIDJSON_BUILD_EXAMPLES=OFF ^ + -DRAPIDJSON_BUILD_TESTS=OFF ^ + ../ || EXIT /B %ERRORLEVEL% +ninja && ninja install || ninja && ninja install || EXIT /B %ERRORLEVEL% +ENDLOCAL diff --git a/src/settings_list.rs b/src/settings_list.rs index 93e4d7c..54bf067 100644 --- a/src/settings_list.rs +++ b/src/settings_list.rs @@ -76,6 +76,8 @@ pub fn get_settings_list() -> Vec { SettingDes::new("push-task-max-push-count", gettext("The maximum number of tasks to push to client at the same time."), JsonValueType::Number, Some(check_nozero_usize)).unwrap(), SettingDes::new("fanbox-http-headers", gettext("Extra http headers for fanbox.cc."), JsonValueType::Object, Some(check_header_map)).unwrap(), SettingDes::new("log-cfg", gettext("The path to the config file of log4rs."), JsonValueType::Str, None).unwrap(), + #[cfg(feature = "server")] + SettingDes::new("ffprobe", gettext("The path to ffprobe executable."), JsonValueType::Str, None).unwrap(), ] } diff --git a/ugoira/CMakeLists.txt b/ugoira/CMakeLists.txt index 44cdce5..3898eac 100644 --- a/ugoira/CMakeLists.txt +++ b/ugoira/CMakeLists.txt @@ -6,6 +6,7 @@ if (MSVC) add_compile_options(/utf-8) endif() +include(CheckIncludeFiles) include(GNUInstallDirs) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake") @@ -15,10 +16,14 @@ find_package(AVFORMAT REQUIRED) find_package(AVCODEC REQUIRED) find_package(SWSCALE REQUIRED) find_package(LIBZIP REQUIRED) +find_package(RapidJSON REQUIRED) option(UTILS_LIBRARY "The path of utils of library." "") if (UTILS_LIBRARY) - set(UTILS_TARGET "${UTILS_LIBRARY}") + find_library(UTILS_LIB utils PATHS "${UTILS_LIBRARY}") + add_library(utils_imported STATIC IMPORTED) + set_property(TARGET utils_imported PROPERTY IMPORTED_LOCATION "${UTILS_LIB}") + set(UTILS_TARGET utils_imported) else() set(ENABLE_ICONV OFF CACHE BOOL "Libiconv is not needed.") add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../utils" "${CMAKE_BINARY_DIR}/utils") @@ -27,6 +32,7 @@ endif() include(CheckSymbolExists) if (WIN32) + check_symbol_exists(printf_s "stdio.h" HAVE_PRINTF_S) check_symbol_exists(sscanf_s "stdio.h" HAVE_SSCANF_S) endif() @@ -49,5 +55,16 @@ target_compile_definitions(ugoira PRIVATE BUILD_UGOIRA) get_link_libraries(OUT ugoira) file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/ugoira_dep.txt" "${OUT}") -install(TARGETS ugoira) +add_executable(ugoira_cli src/main.cpp) +CHECK_INCLUDE_FILES(getopt.h HAVE_GETOPT_H) +if (NOT HAVE_GETOPT_H) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../getopt" "${CMAKE_CURRENT_BINARY_DIR}/getopt") + target_include_directories(ugoira_cli PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../getopt") +endif() +target_link_libraries(ugoira_cli ugoira RapidJSON) +target_include_directories(ugoira_cli PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +set_target_properties(ugoira_cli PROPERTIES OUTPUT_NAME "ugoira") +target_compile_features(ugoira_cli PRIVATE cxx_std_17) + +install(TARGETS ugoira ugoira_cli) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ugoira_dep.txt" DESTINATION ${CMAKE_INSTALL_PREFIX}) diff --git a/ugoira/src/main.cpp b/ugoira/src/main.cpp new file mode 100644 index 0000000..d46986c --- /dev/null +++ b/ugoira/src/main.cpp @@ -0,0 +1,292 @@ +#include "err.h" +#include "fileop.h" +#include "getopt.h" +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" +extern "C" { + #include "libavutil/avutil.h" + #include "libavutil/dict.h" + #include "ugoira.h" +} +#include "str_util.h" +#include "ugoira_config.h" +#include "wchar_util.h" + +#include +#include +#include +#if _WIN32 +#include "Windows.h" +#endif + +#if HAVE_PRINTF_S +#define printf printf_s +#endif + +#if HAVE_SSCANF_S +#define sscanf sscanf_s +#endif + +#if _WIN32 +#ifndef _O_BINARY +#define _O_BINARY 0x8000 +#endif +#else +#define _O_BINARY 0 +#endif + +#ifndef _SH_DENYWR +#define _SH_DENYWR 0x20 +#endif + +void print_help() { + printf("%s", "Usage: ugoira [options] INPUT DEST JSON\n\ +Convert pixiv GIF zip to mp4 file.\n\ +\n\ +Options:\n\ + -h, --help Print this help message.\n\ + -M FPS, --max-fps FPS Set maximum FPS. Default: 60fps.\n\ + -m KEY=VALUE --meta KEY=VALUE\n\ + Set metadata.\n\ + -f, --force-yuv420p Force use yuv420p.\n\ + --crf CRF Set Constant Rate Factor. Default: 18.\n\ + -p PRESET, --preset PRESET\n\ + Set x264 encoder preset. Default: slow.\n\ + -l LEVEL, --level LEVEL Set H264 profile level.\n\ + -P PROFILE, --profile PROFILE\n\ + Set H264 profile.\n"); +} + +#define CRF 128 + +int main(int argc, char* argv[]) { +#if _WIN32 + SetConsoleOutputCP(CP_UTF8); + bool have_wargv = false; + int wargc; + char** wargv; + if (wchar_util::getArgv(wargv, wargc)) { + have_wargv = true; + argc = wargc; + argv = wargv; + } +#endif + if (argc == 1) { + print_help(); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + return 0; + } + struct option opts[] = { + { "help", 0, nullptr, 'h' }, + { "max-fps", 1, nullptr, 'M' }, + { "meta", 1, nullptr, 'm' }, + { "force-yuv420p", 0, nullptr, 'f' }, + { "crf", 1, nullptr, CRF }, + { "preset", 1, nullptr, 'p' }, + { "level", 1, nullptr, 'l' }, + { "profile", 1, nullptr, 'P' }, + nullptr, + }; + int c; + std::string shortopts = "-hM:m:fp:l:P:"; + std::string input; + std::string dest; + std::string json; + bool printh = false; + float max_fps = 60; + struct AVDictionary* metadata = nullptr, * options = nullptr; + while ((c = getopt_long(argc, argv, shortopts.c_str(), opts, nullptr)) != -1) { + switch (c) { + case 'h': + printh = true; + break; + case 'M': + if (sscanf(optarg, "%f", &max_fps) != 1) { + printf("Invalid max fps: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return UGOIRA_INVALID_MAX_FPS; + } + break; + case 'm': + if (true) { + std::string opt(optarg); + auto t = str_util::str_split(opt, "=", 2); + if (av_dict_set(&metadata, t.front().c_str(), t.back().c_str(), 0) < 0) { + printf("Failed to set metadata: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + } + break; + case 'f': + if (av_dict_set(&options, "force_yuv420p", "1", 0) < 0) { + printf("Failed to set force_yuv420p: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + break; + case CRF: + if (av_dict_set(&options, "crf", optarg, 0) < 0) { + printf("Failed to set crf: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + break; + case 'p': + if (av_dict_set(&options, "preset", optarg, 0) < 0) { + printf("Failed to set preset: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + break; + case 'l': + if (av_dict_set(&options, "level", optarg, 0) < 0) { + printf("Failed to set level: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + break; + case 'P': + if (av_dict_set(&options, "profile", optarg, 0) < 0) { + printf("Failed to set profile: %s\n", optarg); +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + break; + case 1: + if (input.empty()) { + input = optarg; + } else if (dest.empty()) { + dest = optarg; + } else if (json.empty()) { + json = optarg; + } else { +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + printf("Too much arguments.\n"); + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + break; + case '?': + default: +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + av_dict_free(&metadata); + av_dict_free(&options); + return 1; + } + } +#if _WIN32 + if (have_wargv) wchar_util::freeArgv(wargv, wargc); +#endif + if (printh) { + print_help(); + av_dict_free(&metadata); + av_dict_free(&options); + return 0; + } + size_t size; + if (!fileop::get_file_size(json, size)) { + printf("Failed to get size of JSON file.\n"); + av_dict_free(&metadata); + av_dict_free(&options); + return UGOIRA_OPEN_FILE; + } + int fd; + int err; + if (err = fileop::open(json, fd, O_RDONLY | _O_BINARY, _SH_DENYWR)) { + std::string msg = "Unknown error."; + err::get_errno_message(msg, err); + printf("Failed to open file: %s(%d)\n", msg.c_str(), err); + av_dict_free(&metadata); + av_dict_free(&options); + return UGOIRA_OPEN_FILE; + }; + char* buf = (char*)malloc(size + 1); + if (!buf) { + fileop::close(fd); + printf("Failed to malloc memory."); + av_dict_free(&metadata); + av_dict_free(&options); + return UGOIRA_OOM; + } + FILE* f = fileop::fdopen(fd, "r"); + if (fread(buf, 1, size, f) != size) { + printf("Failed to read JSON file."); + fileop::fclose(f); + free(buf); + av_dict_free(&metadata); + av_dict_free(&options); + return UGOIRA_OPEN_FILE; + } + fileop::fclose(f); + rapidjson::Document d; + d.Parse(buf); + free(buf); + if (d.HasParseError()) { + auto m = rapidjson::GetParseError_En(d.GetParseError()); + printf("Failed to parse JSON: %s\n", m); + av_dict_free(&metadata); + av_dict_free(&options); + return UGOIRA_JSON_ERROR; + } + UgoiraFrame* top = nullptr, *tail = nullptr; + auto arr = d.GetArray(); + for (auto i = arr.Begin(); i != arr.End(); i++) { + auto obj = i->GetObject(); + auto file = obj["file"].GetString(); + auto delay = obj["delay"].GetFloat(); + tail = new_ugoira_frame(file, delay, tail); + if (!tail) { + if (top) { + free_ugoira_frames(top); + } + av_dict_free(&metadata); + av_dict_free(&options); + printf("Failed to alloc memory for ugoira frame.\n"); + return UGOIRA_OOM; + } + if (!top) { + top = tail; + } + } + auto e = convert_ugoira_to_mp4(input.c_str(), dest.c_str(), top, max_fps, options, metadata); + free_ugoira_frames(top); + av_dict_free(&metadata); + av_dict_free(&options); + return e.code; +} diff --git a/ugoira/ugoira.h b/ugoira/ugoira.h index 8eca385..3cb0b66 100644 --- a/ugoira/ugoira.h +++ b/ugoira/ugoira.h @@ -13,6 +13,7 @@ #define UGOIRA_NO_AVAILABLE_ENCODER 10 #define UGOIRA_OPEN_FILE 11 #define UGOIRA_UNABLE_SCALE 12 +#define UGOIRA_JSON_ERROR 13 typedef struct UgoiraFrame { char* file; float delay; diff --git a/ugoira/ugoira_config.h.in b/ugoira/ugoira_config.h.in index d13e691..8ea945d 100644 --- a/ugoira/ugoira_config.h.in +++ b/ugoira/ugoira_config.h.in @@ -1,4 +1,5 @@ #ifndef _UGOIRA_CONFIG_H #define _UGOIRA_CONFIG_H #cmakedefine HAVE_SSCANF_S @HAVE_SSCANF_S@ +#cmakedefine HAVE_PRINTF_S @HAVE_PRINTF_S@ #endif