diff options
Diffstat (limited to 'test/sprite/sprite_atlas.test.cpp')
-rw-r--r-- | test/sprite/sprite_atlas.test.cpp | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/test/sprite/sprite_atlas.test.cpp b/test/sprite/sprite_atlas.test.cpp new file mode 100644 index 0000000000..f84e17cbcf --- /dev/null +++ b/test/sprite/sprite_atlas.test.cpp @@ -0,0 +1,419 @@ +#include <mbgl/test/util.hpp> +#include <mbgl/test/fixture_log_observer.hpp> +#include <mbgl/test/stub_file_source.hpp> +#include <mbgl/test/stub_style_observer.hpp> + +#include <mbgl/sprite/sprite_atlas.hpp> +#include <mbgl/sprite/sprite_atlas.hpp> +#include <mbgl/sprite/sprite_parser.hpp> +#include <mbgl/util/io.hpp> +#include <mbgl/util/image.hpp> +#include <mbgl/util/run_loop.hpp> +#include <mbgl/util/string.hpp> + +#include <utility> + +using namespace mbgl; + +namespace { + +auto readImage(const std::string& name) { + return decodeImage(util::read_file(name)); +} + +auto imageFromAtlas(const SpriteAtlas& atlas) { + const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; + auto data = std::make_unique<uint8_t[]>(bytes); + const auto src = reinterpret_cast<const uint8_t*>(atlas.getData()); + std::copy(src, src + bytes, data.get()); + return PremultipliedImage{ atlas.getTextureWidth(), atlas.getTextureHeight(), std::move(data) }; +} + +} // namespace + +TEST(SpriteAtlas, Basic) { + FixtureLog log; + + auto spriteParseResult = parseSprite(util::read_file("test/fixtures/annotations/emerald.png"), + util::read_file("test/fixtures/annotations/emerald.json")); + + SpriteAtlas atlas(63, 112, 1); + atlas.setSprites(spriteParseResult.get<Sprites>()); + + EXPECT_EQ(1.0f, atlas.getPixelRatio()); + EXPECT_EQ(63, atlas.getWidth()); + EXPECT_EQ(112, atlas.getHeight()); + EXPECT_EQ(63, atlas.getTextureWidth()); + EXPECT_EQ(112, atlas.getTextureHeight()); + + // Image hasn't been created yet. + EXPECT_FALSE(atlas.getData()); + + auto metro = *atlas.getImage("metro", SpritePatternMode::Single); + EXPECT_EQ(0, metro.pos.x); + EXPECT_EQ(0, metro.pos.y); + EXPECT_EQ(20, metro.pos.w); + EXPECT_EQ(20, metro.pos.h); + EXPECT_EQ(18, metro.spriteImage->getWidth()); + EXPECT_EQ(18, metro.spriteImage->getHeight()); + EXPECT_EQ(18u, metro.spriteImage->image.width); + EXPECT_EQ(18u, metro.spriteImage->image.height); + EXPECT_EQ(1.0f, metro.spriteImage->pixelRatio); + + EXPECT_TRUE(atlas.getData()); + + auto pos = *atlas.getPosition("metro", SpritePatternMode::Single); + EXPECT_DOUBLE_EQ(18, pos.size[0]); + EXPECT_DOUBLE_EQ(18, pos.size[1]); + EXPECT_DOUBLE_EQ(1.0f / 63, pos.tl[0]); + EXPECT_DOUBLE_EQ(1.0f / 112, pos.tl[1]); + EXPECT_DOUBLE_EQ(19.0f / 63, pos.br[0]); + EXPECT_DOUBLE_EQ(19.0f / 112, pos.br[1]); + + auto missing = atlas.getImage("doesnotexist", SpritePatternMode::Single); + EXPECT_FALSE(missing); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Info, + Event::Sprite, + int64_t(-1), + "Can't find sprite named 'doesnotexist'", + })); + + // Different wrapping mode produces different image. + auto metro2 = *atlas.getImage("metro", SpritePatternMode::Repeating); + EXPECT_EQ(20, metro2.pos.x); + EXPECT_EQ(0, metro2.pos.y); + EXPECT_EQ(20, metro2.pos.w); + EXPECT_EQ(20, metro2.pos.h); + + EXPECT_EQ(readImage("test/fixtures/annotations/result-spriteatlas.png"), imageFromAtlas(atlas)); +} + +TEST(SpriteAtlas, Size) { + auto spriteParseResult = parseSprite(util::read_file("test/fixtures/annotations/emerald.png"), + util::read_file("test/fixtures/annotations/emerald.json")); + + SpriteAtlas atlas(63, 112, 1.4); + atlas.setSprites(spriteParseResult.get<Sprites>()); + + EXPECT_DOUBLE_EQ(1.4f, atlas.getPixelRatio()); + EXPECT_EQ(63, atlas.getWidth()); + EXPECT_EQ(112, atlas.getHeight()); + EXPECT_EQ(89, atlas.getTextureWidth()); + EXPECT_EQ(157, atlas.getTextureHeight()); + + auto metro = *atlas.getImage("metro", SpritePatternMode::Single); + EXPECT_EQ(0, metro.pos.x); + EXPECT_EQ(0, metro.pos.y); + EXPECT_EQ(16, metro.pos.w); + EXPECT_EQ(16, metro.pos.h); + EXPECT_EQ(18, metro.spriteImage->getWidth()); + EXPECT_EQ(18, metro.spriteImage->getHeight()); + EXPECT_EQ(18u, metro.spriteImage->image.width); + EXPECT_EQ(18u, metro.spriteImage->image.height); + EXPECT_EQ(1.0f, metro.spriteImage->pixelRatio); + + EXPECT_EQ(readImage("test/fixtures/annotations/result-spriteatlassize.png"), + imageFromAtlas(atlas)); +} + +TEST(SpriteAtlas, Updates) { + SpriteAtlas atlas(32, 32, 1); + + EXPECT_EQ(1.0f, atlas.getPixelRatio()); + EXPECT_EQ(32, atlas.getWidth()); + EXPECT_EQ(32, atlas.getHeight()); + EXPECT_EQ(32, atlas.getTextureWidth()); + EXPECT_EQ(32, atlas.getTextureHeight()); + + atlas.setSprite("one", std::make_shared<SpriteImage>(PremultipliedImage(16, 12), 1)); + auto one = *atlas.getImage("one", SpritePatternMode::Single); + EXPECT_EQ(0, one.pos.x); + EXPECT_EQ(0, one.pos.y); + EXPECT_EQ(20, one.pos.w); + EXPECT_EQ(16, one.pos.h); + EXPECT_EQ(16, one.spriteImage->getWidth()); + EXPECT_EQ(12, one.spriteImage->getHeight()); + EXPECT_EQ(16u, one.spriteImage->image.width); + EXPECT_EQ(12u, one.spriteImage->image.height); + EXPECT_EQ(1.0f, one.spriteImage->pixelRatio); + + EXPECT_EQ(readImage("test/fixtures/annotations/result-spriteatlas-empty.png"), + imageFromAtlas(atlas)); + + // Update sprite + PremultipliedImage image2(16, 12); + for (size_t i = 0; i < image2.size(); i++) { + image2.data.get()[i] = 255; + } + auto newSprite = std::make_shared<SpriteImage>(std::move(image2), 1); + atlas.setSprite("one", newSprite); + ASSERT_EQ(newSprite, atlas.getSprite("one")); + + // Atlas texture hasn't changed yet. + EXPECT_EQ(readImage("test/fixtures/annotations/result-spriteatlas-empty.png"), + imageFromAtlas(atlas)); + + atlas.updateDirty(); + + // Now the atlas texture has changed. + EXPECT_EQ(readImage("test/fixtures/annotations/result-spriteatlas-updated.png"), + imageFromAtlas(atlas)); +} + +TEST(SpriteAtlas, AddRemove) { + FixtureLog log; + + const auto sprite1 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + const auto sprite2 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + const auto sprite3 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + + SpriteAtlas atlas(32, 32, 1); + + // Adding single + atlas.setSprite("one", sprite1); + + // Adding multiple + atlas.setSprite("two", sprite2); + atlas.setSprite("three", sprite3); + + // Removing + atlas.removeSprite("one"); + atlas.removeSprite("two"); + + // Accessing + EXPECT_EQ(sprite3, atlas.getSprite("three")); + + EXPECT_TRUE(log.empty()); + + EXPECT_EQ(nullptr, atlas.getSprite("two")); + EXPECT_EQ(nullptr, atlas.getSprite("four")); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Info, + Event::Sprite, + int64_t(-1), + "Can't find sprite named 'two'", + })); + EXPECT_EQ(1u, log.count({ + EventSeverity::Info, + Event::Sprite, + int64_t(-1), + "Can't find sprite named 'four'", + })); + + // Overwriting + atlas.setSprite("three", sprite1); +} + +TEST(SpriteAtlas, OtherPixelRatio) { + FixtureLog log; + + const auto sprite1 = std::make_shared<SpriteImage>(PremultipliedImage(8, 8), 1); + + SpriteAtlas atlas(32, 32, 1); + + // Adding mismatched sprite image + atlas.setSprite("one", sprite1); +} + +TEST(SpriteAtlas, Multiple) { + const auto sprite1 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + const auto sprite2 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + + SpriteAtlas atlas(32, 32, 1); + + atlas.setSprites({ + { "one", sprite1 }, { "two", sprite2 }, + }); +} + +TEST(SpriteAtlas, Replace) { + FixtureLog log; + + const auto sprite1 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + const auto sprite2 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + + SpriteAtlas atlas(32, 32, 1); + + atlas.setSprite("sprite", sprite1); + EXPECT_EQ(sprite1, atlas.getSprite("sprite")); + atlas.setSprite("sprite", sprite2); + EXPECT_EQ(sprite2, atlas.getSprite("sprite")); +} + +TEST(SpriteAtlas, ReplaceWithDifferentDimensions) { + FixtureLog log; + + PremultipliedImage image(16, 16); + PremultipliedImage image2(18, 18); + const auto sprite1 = std::make_shared<SpriteImage>(PremultipliedImage(16, 16), 2); + const auto sprite2 = std::make_shared<SpriteImage>(PremultipliedImage(18, 18), 2); + + SpriteAtlas atlas(32, 32, 1); + + atlas.setSprite("sprite", sprite1); + atlas.setSprite("sprite", sprite2); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't change sprite dimensions for 'sprite'", + })); + + EXPECT_EQ(sprite1, atlas.getSprite("sprite")); +} + +class SpriteAtlasTest { +public: + SpriteAtlasTest() = default; + + util::RunLoop loop; + StubFileSource fileSource; + StubStyleObserver observer; + SpriteAtlas spriteAtlas { 32, 32, 1 }; + + void run() { + // Squelch logging. + Log::setObserver(std::make_unique<Log::NullObserver>()); + + spriteAtlas.setObserver(&observer); + spriteAtlas.load("test/fixtures/resources/sprite", fileSource); + + loop.run(); + } + + void end() { + loop.stop(); + } +}; + +Response successfulSpriteImageResponse(const Resource& resource) { + EXPECT_EQ("test/fixtures/resources/sprite.png", resource.url); + Response response; + response.data = std::make_shared<std::string>(util::read_file(resource.url)); + return response; +} + +Response successfulSpriteJSONResponse(const Resource& resource) { + EXPECT_EQ("test/fixtures/resources/sprite.json", resource.url); + Response response; + response.data = std::make_shared<std::string>(util::read_file(resource.url)); + return response; +} + +Response failedSpriteResponse(const Resource&) { + Response response; + response.error = std::make_unique<Response::Error>( + Response::Error::Reason::Other, + "Failed by the test case"); + return response; +} + +Response corruptSpriteResponse(const Resource&) { + Response response; + response.data = std::make_shared<std::string>("CORRUPT"); + return response; +} + +TEST(SpriteAtlas, LoadingSuccess) { + SpriteAtlasTest test; + + test.fileSource.spriteImageResponse = successfulSpriteImageResponse; + test.fileSource.spriteJSONResponse = successfulSpriteJSONResponse; + + test.observer.spriteError = [&] (std::exception_ptr error) { + FAIL() << util::toString(error); + test.end(); + }; + + test.observer.spriteLoaded = [&] () { + EXPECT_EQ(1.0, test.spriteAtlas.getPixelRatio()); + EXPECT_TRUE(test.spriteAtlas.isLoaded()); + test.end(); + }; + + test.run(); +} + +TEST(SpriteAtlas, JSONLoadingFail) { + SpriteAtlasTest test; + + test.fileSource.spriteImageResponse = successfulSpriteImageResponse; + test.fileSource.spriteJSONResponse = failedSpriteResponse; + + test.observer.spriteError = [&] (std::exception_ptr error) { + EXPECT_TRUE(error != nullptr); + EXPECT_EQ("Failed by the test case", util::toString(error)); + EXPECT_FALSE(test.spriteAtlas.isLoaded()); + test.end(); + }; + + test.run(); +} + +TEST(SpriteAtlas, ImageLoadingFail) { + SpriteAtlasTest test; + + test.fileSource.spriteImageResponse = failedSpriteResponse; + test.fileSource.spriteJSONResponse = successfulSpriteJSONResponse; + + test.observer.spriteError = [&] (std::exception_ptr error) { + EXPECT_TRUE(error != nullptr); + EXPECT_EQ("Failed by the test case", util::toString(error)); + EXPECT_FALSE(test.spriteAtlas.isLoaded()); + test.end(); + }; + + test.run(); +} + +TEST(SpriteAtlas, JSONLoadingCorrupted) { + SpriteAtlasTest test; + + test.fileSource.spriteImageResponse = successfulSpriteImageResponse; + test.fileSource.spriteJSONResponse = corruptSpriteResponse; + + test.observer.spriteError = [&] (std::exception_ptr error) { + EXPECT_TRUE(error != nullptr); + EXPECT_EQ("Failed to parse JSON: Invalid value. at offset 0", util::toString(error)); + EXPECT_FALSE(test.spriteAtlas.isLoaded()); + test.end(); + }; + + test.run(); +} + +TEST(SpriteAtlas, ImageLoadingCorrupted) { + SpriteAtlasTest test; + + test.fileSource.spriteImageResponse = corruptSpriteResponse; + test.fileSource.spriteJSONResponse = successfulSpriteJSONResponse; + + test.observer.spriteError = [&] (std::exception_ptr error) { + EXPECT_TRUE(error != nullptr); + // Not asserting on platform-specific error text. + EXPECT_FALSE(test.spriteAtlas.isLoaded()); + test.end(); + }; + + test.run(); +} + +TEST(SpriteAtlas, LoadingCancel) { + SpriteAtlasTest test; + + test.fileSource.spriteImageResponse = + test.fileSource.spriteJSONResponse = [&] (const Resource&) { + test.end(); + return optional<Response>(); + }; + + test.observer.spriteLoaded = [&] () { + FAIL() << "Should never be called"; + }; + + test.run(); +} |