diff options
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | anim/atlas.cpp | 18 | ||||
| -rw-r--r-- | anim/atlas.hpp | 13 | ||||
| -rw-r--r-- | anim/serialize.cpp | 48 | ||||
| -rw-r--r-- | anim/serialize.hpp | 10 | ||||
| -rw-r--r-- | crop-tool/crop-tool.cpp | 169 | ||||
| -rw-r--r-- | doc/atlas.json | 18 | ||||
| -rw-r--r-- | doc/atlas.json.example | 36 | ||||
| -rw-r--r-- | include/json.hpp (renamed from json.hpp) | 0 |
9 files changed, 176 insertions, 138 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index d5245256..9d5a8477 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,8 @@ if(NOT BOOTSTRAP_DEPENDS) find_package(MagnumIntegration QUIET REQUIRED COMPONENTS Glm) include_directories("${CMAKE_CURRENT_SOURCE_DIR}") + include_directories(SYSTEM "./include") + add_subdirectory(crop-tool) add_subdirectory(anim) diff --git a/anim/atlas.cpp b/anim/atlas.cpp index ca05cd15..fe4b5f65 100644 --- a/anim/atlas.cpp +++ b/anim/atlas.cpp @@ -1,41 +1,43 @@ +#undef NDEBUG + #include "atlas.hpp" #include "serialize.hpp" -#include "../defs.hpp" +#include <cassert> #include <filesystem> #include <opencv2/imgcodecs.hpp> -void anim_atlas_row::add_entry(const anim_atlas_entry& x) +void anim_atlas_row::add_entry(const anim_atlas_entry& x) noexcept { auto& frame = *x.frame; const auto& mat = x.mat; frame.offset = {xpos, ypos}; frame.size = {mat.cols, mat.rows}; - ASSERT(mat.rows > 0 && mat.cols > 0); + assert(mat.rows > 0 && mat.cols > 0); data.push_back(x); xpos += mat.cols; max_height = std::max(mat.rows, max_height); } -void anim_atlas::advance_row() +void anim_atlas::advance_row() noexcept { auto& row = rows.back(); if (row.data.empty()) return; - ASSERT(row.xpos); ASSERT(row.max_height); + assert(row.xpos); assert(row.max_height); ypos += row.max_height; maxx = std::max(row.xpos, maxx); rows.push_back({{}, 0, 0, ypos}); } -Magnum::Vector2i anim_atlas::offset() const +Magnum::Vector2i anim_atlas::offset() const noexcept { const auto& row = rows.back(); return {row.xpos, row.ypos}; } -Magnum::Vector2i anim_atlas::size() const +Magnum::Vector2i anim_atlas::size() const noexcept { const anim_atlas_row& row = rows.back(); // prevent accidentally writing out of bounds by forgetting to call @@ -43,7 +45,7 @@ Magnum::Vector2i anim_atlas::size() const return {std::max(maxx, row.xpos), ypos + row.max_height}; } -bool anim_atlas::dump(const std::filesystem::path& filename) const +bool anim_atlas::dump(const std::filesystem::path& filename) const noexcept { auto sz = size(); cv::Mat4b mat(sz[1], sz[0]); diff --git a/anim/atlas.hpp b/anim/atlas.hpp index dd6efabc..5c5e918f 100644 --- a/anim/atlas.hpp +++ b/anim/atlas.hpp @@ -1,4 +1,5 @@ #pragma once + #include <vector> #include <Magnum/Magnum.h> #include <Magnum/Math/Vector2.h> @@ -19,7 +20,7 @@ struct anim_atlas_row std::vector<anim_atlas_entry> data; int max_height = 0, xpos = 0, ypos = 0; - void add_entry(const anim_atlas_entry& x); + void add_entry(const anim_atlas_entry& x) noexcept; }; class anim_atlas @@ -28,9 +29,9 @@ class anim_atlas int ypos = 0, maxx = 0; public: - void add_entry(const anim_atlas_entry& x) { rows.back().add_entry(x); } - void advance_row(); - Magnum::Vector2i offset() const; - Magnum::Vector2i size() const; - [[nodiscard]] bool dump(const std::filesystem::path& filename) const; + void add_entry(const anim_atlas_entry& x) noexcept { rows.back().add_entry(x); } + void advance_row() noexcept; + Magnum::Vector2i offset() const noexcept; + Magnum::Vector2i size() const noexcept; + [[nodiscard]] bool dump(const std::filesystem::path& filename) const noexcept; }; diff --git a/anim/serialize.cpp b/anim/serialize.cpp index 7e6d738a..92ff1481 100644 --- a/anim/serialize.cpp +++ b/anim/serialize.cpp @@ -1,5 +1,5 @@ #include "serialize.hpp" -#include "../json.hpp" +#include "json.hpp" #include <algorithm> #include <utility> @@ -43,11 +43,21 @@ void adl_serializer<Magnum::Vector2i>::from_json(const json& j, Magnum::Vector2i } // namespace nlohmann -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_frame, ground, offset, size); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_group, name, frames, ground); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim, name, nframes, actionframe, fps, groups); +#if defined __clang__ || defined __CLION_IDE__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wweak-vtables" +# pragma clang diagnostic ignored "-Wcovered-switch-default" +#endif -std::tuple<anim, bool> anim::from_json(const std::filesystem::path& pathname) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_frame, ground, offset, size) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim_group, name, frames, ground) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(anim, name, nframes, actionframe, fps, groups, width, height) + +#if defined __clang__ || defined __CLION_IDE__ +# pragma clang diagnostic pop +#endif + +std::tuple<anim, bool> anim::from_json(const std::filesystem::path& pathname) noexcept { using namespace nlohmann; std::ifstream s; @@ -55,7 +65,7 @@ std::tuple<anim, bool> anim::from_json(const std::filesystem::path& pathname) try { s.open(pathname, std::ios_base::in); } catch (const std::ios::failure& e) { - Error{Error::Flag::NoSpace} << "failed to open '" << pathname << "':" << e.what(); + Error{Error::Flag::NoSpace} << "failed to open " << pathname << ": " << e.what(); return { {}, false }; } anim ret; @@ -65,29 +75,29 @@ std::tuple<anim, bool> anim::from_json(const std::filesystem::path& pathname) using nlohmann::from_json; from_json(j, ret); } catch (const std::exception& e) { - Error{Error::Flag::NoSpace} << "failed to parse '" << pathname << "':" << e.what(); + Error{Error::Flag::NoSpace} << "failed to parse " << pathname << ": " << e.what(); return { {}, false }; } return { std::move(ret), true }; } -bool anim::to_json(const std::filesystem::path& pathname) +bool anim::to_json(const std::filesystem::path& pathname) noexcept { - nlohmann::json j = *this; - - std::ofstream s; - s.exceptions(s.exceptions() | std::ios::failbit | std::ios::badbit); - try { - s.open(pathname, std::ios_base::out | std::ios_base::trunc); - } catch (const std::ios::failure& e) { - Error{Error::Flag::NoSpace} << "failed to open '" << pathname << "' for writing: " << e.what(); - return false; - } try { + nlohmann::json j = *this; + + std::ofstream s; + s.exceptions(s.exceptions() | std::ios::failbit | std::ios::badbit); + try { + s.open(pathname, std::ios_base::out | std::ios_base::trunc); + } catch (const std::ios::failure& e) { + Error{} << "failed to open" << pathname << "for writing:" << e.what(); + return false; + } s << j.dump(4); s.flush(); } catch (const std::exception& e) { - Error{Error::Flag::NoSpace} << "failed writing '" << pathname << "' :" << e.what(); + Error{Error::Flag::NoSpace} << "failed writing to " << pathname << ": " << e.what(); return false; } diff --git a/anim/serialize.hpp b/anim/serialize.hpp index 8c049978..a8b13d41 100644 --- a/anim/serialize.hpp +++ b/anim/serialize.hpp @@ -19,7 +19,7 @@ struct anim_frame final enum class anim_direction : unsigned char { N, NE, E, SE, S, SW, W, NW, - COUNT = NW + 1, + COUNT, }; struct anim_group final @@ -31,11 +31,13 @@ struct anim_group final struct anim final { - static std::tuple<anim, bool> from_json(const std::filesystem::path& pathname); - [[nodiscard]] bool to_json(const std::filesystem::path& pathname); + static std::tuple<anim, bool> from_json(const std::filesystem::path& pathname) noexcept; + [[nodiscard]] bool to_json(const std::filesystem::path& pathname) noexcept; static constexpr int default_fps = 24; std::string name; std::array<anim_group, (std::size_t)anim_direction::COUNT> groups; - int nframes = 0, actionframe = -1, fps = default_fps; + int nframes = 0; + int width = 0, height = 0; + int actionframe = -1, fps = default_fps; }; diff --git a/crop-tool/crop-tool.cpp b/crop-tool/crop-tool.cpp index fd4d8c01..4dc2c3e8 100644 --- a/crop-tool/crop-tool.cpp +++ b/crop-tool/crop-tool.cpp @@ -1,7 +1,19 @@ +#undef NDEBUG + #include "defs.hpp" #include "anim/atlas.hpp" #include "anim/serialize.hpp" +#include <cassert> +#include <cmath> +#include <cstring> + +#include <algorithm> +#include <utility> +#include <tuple> + +#include <filesystem> + #include <Corrade/Utility/Arguments.h> #include <Corrade/Utility/Debug.h> #include <Corrade/Utility/DebugStl.h> @@ -10,14 +22,6 @@ #include <opencv2/imgcodecs/imgcodecs.hpp> #include <opencv2/imgproc/imgproc.hpp> -#include <algorithm> -#include <utility> -#include <tuple> - -#include <cmath> -#include <cstring> -#include <filesystem> - using Corrade::Utility::Error; using Corrade::Utility::Debug; @@ -25,24 +29,25 @@ using std::filesystem::path; struct options { - unsigned width = 0, height = 0; double scale = 0; - path input_dir, output_dir; + path input_dir, input_file, output_dir; + int width = 0, height = 0, nframes = 0; }; [[nodiscard]] -static std::tuple<cv::Vec2i, cv::Vec2i, bool> find_image_bounds(const path& path, const cv::Mat4b& mat) +static std::tuple<cv::Vec2i, cv::Vec2i, bool> find_image_bounds(const cv::Mat4b& mat) noexcept { cv::Vec2i start{mat.cols, mat.rows}, end{0, 0}; + bool ok = false; for (int y = 0; y < mat.rows; y++) { const auto* ptr = mat.ptr<cv::Vec4b>(y); for (int x = 0; x < mat.cols; x++) { enum {R, G, B, A}; - cv::Vec4b px = ptr[x]; - if (px[A] != 0) + if (cv::Vec4b px = ptr[x]; px[A] != 0) { + ok = true; start[0] = std::min(x, start[0]); start[1] = std::min(y, start[1]); end[0] = std::max(x+1, end[0]); @@ -50,46 +55,46 @@ static std::tuple<cv::Vec2i, cv::Vec2i, bool> find_image_bounds(const path& path } } } - if (start[0] >= end[0] || start[1] >= end[1]) - { - Error{Error::Flag::NoSpace} << "image '" << path << "' contains only fully transparent pixels!"; + if (ok) + return {start, end, true}; + else return {{}, {}, false}; - } - - return {start, end, true}; } [[nodiscard]] -static bool load_file(anim_group& group, options& opts, anim_atlas& atlas, const path& filename) +static bool load_file(anim_group& group, options& opts, anim_atlas& atlas, const path& filename) noexcept { auto mat = progn( - cv::Mat mat_ = cv::imread(filename.string(), cv::IMREAD_UNCHANGED); - if (mat_.empty() || mat_.type() != CV_8UC4) + cv::Mat mat = cv::imread(filename.string(), cv::IMREAD_UNCHANGED); + if (mat.empty() || mat.type() != CV_8UC4) { - Error{Error::Flag::NoSpace} << "failed to load '" << filename << "' as RGBA32 image"; + Error{} << "failed to load" << filename << "as RGBA32 image"; return cv::Mat4b{}; } - return cv::Mat4b(std::move(mat_)); + return cv::Mat4b(std::move(mat)); ); if (mat.empty()) return false; - auto [start, end, bounds_ok] = find_image_bounds(filename, mat); + auto [start, end, bounds_ok] = find_image_bounds(mat); if (!bounds_ok) + { + Error{} << "no valid image data in" << filename; return false; + } cv::Size size{end - start}; if (opts.scale == 0.0) { - ASSERT(opts.width || opts.height); + assert(opts.width || opts.height); if (opts.width) opts.scale = (double)opts.width / size.width; else opts.scale = (double)opts.height / size.height; - ASSERT(opts.scale > 1e-6); + assert(opts.scale > 1e-6); } const cv::Size dest_size = { @@ -99,7 +104,7 @@ static bool load_file(anim_group& group, options& opts, anim_atlas& atlas, const if (size.width < dest_size.width || size.height < dest_size.height) { - Error{Error::Flag::NoSpace} << "refusing to upscale image '" << filename << "'"; + Error{} << "refusing to upscale image" << filename; return false; } @@ -117,31 +122,42 @@ static bool load_file(anim_group& group, options& opts, anim_atlas& atlas, const } [[nodiscard]] -static bool load_directory(anim_group& group, options& opts, anim_atlas& atlas, const path& input_dir) +static bool load_directory(anim_group& group, options& opts, anim_atlas& atlas, const path& input_dir) noexcept { if (std::error_code ec; !std::filesystem::exists(input_dir/".", ec)) { - Error{Error::Flag::NoSpace} << "can't open directory '" << input_dir << "':" << ec.message(); + Error{Error::Flag::NoSpace} << "can't open directory " << input_dir << ": " << ec.message(); return false; } - std::size_t max; + int max; for (max = 1; max <= 9999; max++) { char filename[9]; - sprintf(filename, "%04zu.png", max); - if (!std::filesystem::exists(input_dir/filename)) + sprintf(filename, "%04d.png", max); + if (std::error_code ec; !std::filesystem::exists(input_dir/filename, ec)) break; } + + if (!opts.nframes) + opts.nframes = max-1; + else if (opts.nframes != max-1) + { + Error{Error::Flag::NoSpace} << "wrong frame count for direction '" + << group.name << "', " << max-1 + << " should be " << opts.nframes; + return false; + } + group.frames.clear(); // atlas stores its entries through a pointer. // vector::reserve() is necessary to avoid use-after-free. - group.frames.reserve(max-1); + group.frames.reserve((std::size_t)max-1); - for (std::size_t i = 1; i < max; i++) + for (int i = 1; i < max; i++) { char filename[9]; - sprintf(filename, "%04zu.png", i); + sprintf(filename, "%04d.png", i); if (!load_file(group, opts, atlas, input_dir/filename)) return false; } @@ -151,7 +167,7 @@ static bool load_directory(anim_group& group, options& opts, anim_atlas& atlas, return true; } -static char* fix_argv0(char* argv0) +static char* fix_argv0(char* argv0) noexcept { #ifdef _WIN32 if (auto* c = strrchr(argv0, '\\'); c && c[1]) @@ -167,67 +183,90 @@ static char* fix_argv0(char* argv0) return argv0; } -static std::tuple<options, bool> parse_cmdline(int argc, const char* const* argv) +using Corrade::Utility::Arguments; + +static std::tuple<options, Arguments, bool> parse_cmdline(int argc, const char* const* argv) noexcept { Corrade::Utility::Arguments args{}; args.addOption('o', "output") - .addArgument("directory") + .addArgument("input") .addOption('W', "width", "") .addOption('H', "height", ""); args.parse(argc, argv); options opts; - if (unsigned w = args.value<unsigned>("width"); w != 0) + if (int w = args.value<int>("width"); w != 0) opts.width = w; - if (unsigned h = args.value<unsigned>("height"); h != 0) + if (int h = args.value<int>("height"); h != 0) opts.height = h; - if (!(!opts.width ^ !opts.height)) - { - Error{} << "exactly one of --width, --height must be given"; - goto usage; - } - opts.output_dir = args.value<std::string>("output"); - opts.input_dir = args.value<std::string>("directory"); + opts.input_file = args.value<std::string>("input"); + opts.input_dir = opts.input_file.parent_path(); if (opts.output_dir.empty()) opts.output_dir = opts.input_dir; - return { std::move(opts), true }; -usage: + return { std::move(opts), std::move(args), true }; +} + +[[nodiscard]] static int usage(const Arguments& args) noexcept +{ Error{Error::Flag::NoNewlineAtTheEnd} << args.usage(); - return { {}, false }; + return EX_USAGE; +} + +[[nodiscard]] static bool check_atlas_name(const std::string& str) noexcept +{ + constexpr auto npos = std::string::npos; + + if (str.empty()) + return false; + if (str[0] == '.' || str[0] == '\\' || str[0] == '/') + return false; + if (str.find('"') != npos || str.find('\'') != npos) + return false; + if (str.find("/.") != npos || str.find("\\.") != npos) + return false; // NOLINT(readability-simplify-boolean-expr) + + return true; } int main(int argc, char** argv) { argv[0] = fix_argv0(argv[0]); - auto [opts, opts_ok] = parse_cmdline(argc, argv); + auto [opts, args, opts_ok] = parse_cmdline(argc, argv); if (!opts_ok) - return EX_USAGE; + return usage(args); - auto [anim_info, anim_ok] = anim::from_json(opts.input_dir/"atlas.json"); + auto [anim_info, anim_ok] = anim::from_json(opts.input_file); if (!anim_ok) return EX_DATAERR; - if (std::error_code error; - !std::filesystem::exists(opts.output_dir/".") && - !std::filesystem::create_directory(opts.output_dir, error)) + if (!check_atlas_name(anim_info.name)) { - Error{Error::Flag::NoSpace} << "failed to create output directory '" << opts.output_dir << "':" - << error.message(); - return EX_CANTCREAT; + Error{Error::Flag::NoSpace} << "atlas name '" << anim_info.name << "' contains invalid characters"; + return EX_DATAERR; } - anim_atlas atlas; + if (!opts.width) + opts.width = anim_info.width; + if (!opts.height) + opts.height = anim_info.height; + opts.nframes = anim_info.nframes; + + if (!(opts.width ^ opts.height) || opts.width < 0 || opts.height < 0) + { + Error{} << "exactly one of --width, --height must be specified"; + return usage(args); + } - for (anim_group& group : anim_info.groups) + for (anim_atlas atlas; + anim_group& group : anim_info.groups) { - group.frames.clear(); group.frames.reserve(64); if (!load_directory(group, opts, atlas, opts.input_dir/group.name)) return EX_DATAERR; - if (!atlas.dump(opts.output_dir/"atlas.png")) + if (!atlas.dump(opts.output_dir/(anim_info.name + ".png"))) return EX_CANTCREAT; - if (!anim_info.to_json(opts.output_dir/"atlas.json.new")) + if (!anim_info.to_json(opts.output_dir/(anim_info.name + ".json"))) return EX_CANTCREAT; } diff --git a/doc/atlas.json b/doc/atlas.json deleted file mode 100644 index c5b05b15..00000000 --- a/doc/atlas.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "character", - "nframes": 24, - "actionframe": 0, - "fps": 24, - "groups": [ - { "name": "n", "frames": [], "ground": {"x":484, "y":488} }, - { "name": "ne", "frames": [], "ground": {"x": 0, "y": 0} }, - { "name": "e", "frames": [], "ground": {"x": 0, "y": 0} }, - { "name": "se", "frames": [], "ground": {"x": 0, "y": 0} }, - { "name": "s", "frames": [], "ground": {"x": 0, "y": 0} }, - { "name": "sw", "frames": [], "ground": {"x": 0, "y": 0} }, - { "name": "w", "frames": [], "ground": {"x": 0, "y": 0} }, - { "name": "nw", "frames": [], "ground": {"x": 0, "y": 0} } - ] -} - -// vim: ft=javascript diff --git a/doc/atlas.json.example b/doc/atlas.json.example index 9d54b5be..bbb2796f 100644 --- a/doc/atlas.json.example +++ b/doc/atlas.json.example @@ -1,18 +1,18 @@ -{ - "name": "character", - "nframes": 24, - "actionframe": 0, - "fps": 24, - "directions": [ - { "direction": "N", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "NE", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "E", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "SE", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "S", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "SW", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "W", "frames": [], "ground": {"x": 0, "y": 0} }, - { "direction": "NW", "frames": [], "ground": {"x": 0, "y": 0} } - ] -} - -// vim: ft=javascript +{
+ "name": "test",
+ "width": 0, "height": 0,
+ "nframes": 0, "fps": 24,
+ "actionframe": 0,
+
+ "groups": [
+ { "frames": [], "ground": "0 x 0", "name": "n" },
+ { "frames": [], "ground": "0 x 0", "name": "ne" },
+ { "frames": [], "ground": "0 x 0", "name": "e" },
+ { "frames": [], "ground": "0 x 0", "name": "se" },
+ { "frames": [], "ground": "0 x 0", "name": "s" },
+ { "frames": [], "ground": "0 x 0", "name": "sw" },
+ { "frames": [], "ground": "0 x 0", "name": "w" },
+ { "frames": [], "ground": "0 x 0", "name": "nw" }
+ ]
+}
+// vim: ft=javascript
diff --git a/json.hpp b/include/json.hpp index 2837e74b..2837e74b 100644 --- a/json.hpp +++ b/include/json.hpp |
