summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--anim/atlas.cpp18
-rw-r--r--anim/atlas.hpp13
-rw-r--r--anim/serialize.cpp48
-rw-r--r--anim/serialize.hpp10
-rw-r--r--crop-tool/crop-tool.cpp169
-rw-r--r--doc/atlas.json18
-rw-r--r--doc/atlas.json.example36
-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