summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Käfer <mail@kkaefer.com>2015-06-24 17:40:03 +0200
committerKonstantin Käfer <mail@kkaefer.com>2015-07-08 19:45:59 +0200
commita96a8fef767bacb2b1a57c9e4a808d54d87623e3 (patch)
treee943811029f8eda70a8667f693f67cc70374af38
parenta68b47b9699514547ccc529361e01530c3fa05cc (diff)
downloadqtlocation-mapboxgl-a96a8fef767bacb2b1a57c9e4a808d54d87623e3.tar.gz
add a separate SpriteParser object
transforms a JSON and an associated PNG file into individual SpriteImage objects that can be inserted into a SpriteStore
-rw-r--r--src/mbgl/annotation/sprite_parser.cpp147
-rw-r--r--src/mbgl/annotation/sprite_parser.hpp39
-rw-r--r--test/annotations/sprite_parser.cpp309
-rw-r--r--test/fixtures/annotations/emerald.json1
-rw-r--r--test/fixtures/annotations/emerald.pngbin0 -> 39041 bytes
-rw-r--r--test/fixtures/annotations/emerald@2x.json1
-rw-r--r--test/fixtures/annotations/emerald@2x.pngbin0 -> 80896 bytes
-rw-r--r--test/test.gypi1
8 files changed, 498 insertions, 0 deletions
diff --git a/src/mbgl/annotation/sprite_parser.cpp b/src/mbgl/annotation/sprite_parser.cpp
new file mode 100644
index 0000000000..8285eecba5
--- /dev/null
+++ b/src/mbgl/annotation/sprite_parser.cpp
@@ -0,0 +1,147 @@
+#include <mbgl/annotation/sprite_parser.hpp>
+#include <mbgl/annotation/sprite_image.hpp>
+
+#include <mbgl/platform/log.hpp>
+
+#include <mbgl/util/image.hpp>
+
+#include <rapidjson/document.h>
+
+#include <cmath>
+
+namespace mbgl {
+
+SpriteImagePtr createSpriteImage(const util::Image& image,
+ const uint16_t srcX,
+ const uint16_t srcY,
+ const uint16_t srcWidth,
+ const uint16_t srcHeight,
+ const double ratio,
+ const bool) {
+ // Disallow invalid parameter configurations.
+ if (srcWidth == 0 || srcHeight == 0 || ratio <= 0 || ratio > 10 || srcWidth > 1024 ||
+ srcHeight > 1024) {
+ Log::Warning(Event::Sprite, "Can't create sprite with invalid metrics");
+ return nullptr;
+ }
+
+ const uint16_t width = std::ceil(double(srcWidth) / ratio);
+ const uint16_t dstWidth = std::ceil(width * ratio);
+ assert(dstWidth >= srcWidth);
+ const uint16_t height = std::ceil(double(srcHeight) / ratio);
+ const uint16_t dstHeight = std::ceil(height * ratio);
+ assert(dstHeight >= srcHeight);
+
+ std::string data(dstWidth * dstHeight * 4, '\0');
+
+ auto srcData = reinterpret_cast<const uint32_t*>(image.getData());
+ auto dstData = reinterpret_cast<uint32_t*>(const_cast<char*>(data.data()));
+
+ const int32_t maxX = std::min(image.getWidth(), uint32_t(srcWidth + srcX)) - srcX;
+ assert(maxX <= int32_t(image.getWidth()));
+ const int32_t maxY = std::min(image.getHeight(), uint32_t(srcHeight + srcY)) - srcY;
+ assert(maxY <= int32_t(image.getHeight()));
+
+ // Copy from the source image into our individual sprite image
+ for (uint16_t y = 0; y < maxY; ++y) {
+ const auto dstRow = y * dstWidth;
+ const auto srcRow = (y + srcY) * image.getWidth() + srcX;
+ for (uint16_t x = 0; x < maxX; ++x) {
+ dstData[dstRow + x] = srcData[srcRow + x];
+ }
+ }
+
+ return std::make_unique<const SpriteImage>(width, height, ratio, std::move(data));
+}
+
+namespace {
+
+inline uint16_t getUInt16(const rapidjson::Value& value, const char* name, const uint16_t def = 0) {
+ if (value.HasMember(name)) {
+ auto& v = value[name];
+ if (v.IsUint() && v.GetUint() <= std::numeric_limits<uint16_t>::max()) {
+ return v.GetUint();
+ } else {
+ Log::Warning(Event::Sprite, "Value of '%s' must be an integer between 0 and 65535",
+ name);
+ }
+ }
+
+ return def;
+}
+
+inline double getDouble(const rapidjson::Value& value, const char* name, const double def = 0) {
+ if (value.HasMember(name)) {
+ auto& v = value[name];
+ if (v.IsNumber()) {
+ return v.GetDouble();
+ } else {
+ Log::Warning(Event::Sprite, "Value of '%s' must be a number", name);
+ }
+ }
+
+ return def;
+}
+
+inline bool getBoolean(const rapidjson::Value& value, const char* name, const bool def = false) {
+ if (value.HasMember(name)) {
+ auto& v = value[name];
+ if (v.IsBool()) {
+ return v.GetBool();
+ } else {
+ Log::Warning(Event::Sprite, "Value of '%s' must be a boolean", name);
+ }
+ }
+
+ return def;
+}
+
+} // namespace
+
+Sprites parseSprite(const std::string& image, const std::string& json) {
+ using namespace rapidjson;
+
+ Sprites sprites;
+
+ // Parse the sprite image.
+ const util::Image raster(image);
+ if (!raster) {
+ Log::Warning(Event::Sprite, "Could not parse sprite image");
+ return sprites;
+ }
+
+ Document doc;
+ doc.Parse<0>(json.c_str());
+
+ if (doc.HasParseError()) {
+ Log::Warning(Event::Sprite, std::string{ "Failed to parse JSON: " } + doc.GetParseError() +
+ " at offset " + std::to_string(doc.GetErrorOffset()));
+ return sprites;
+ } else if (!doc.IsObject()) {
+ Log::Warning(Event::Sprite, "Sprite JSON root must be an object");
+ return sprites;
+ } else {
+ for (Value::ConstMemberIterator itr = doc.MemberBegin(); itr != doc.MemberEnd(); ++itr) {
+ const std::string name = { itr->name.GetString(), itr->name.GetStringLength() };
+ const Value& value = itr->value;
+
+ if (value.IsObject()) {
+ const uint16_t x = getUInt16(value, "x", 0);
+ const uint16_t y = getUInt16(value, "y", 0);
+ const uint16_t width = getUInt16(value, "width", 0);
+ const uint16_t height = getUInt16(value, "height", 0);
+ const double pixelRatio = getDouble(value, "pixelRatio", 1);
+ const bool sdf = getBoolean(value, "sdf", false);
+
+ auto sprite = createSpriteImage(image, x, y, width, height, pixelRatio, sdf);
+ if (sprite) {
+ sprites.emplace(name, sprite);
+ }
+ }
+ }
+ }
+
+ return sprites;
+}
+
+} // namespace mbgl
diff --git a/src/mbgl/annotation/sprite_parser.hpp b/src/mbgl/annotation/sprite_parser.hpp
new file mode 100644
index 0000000000..71228aacf6
--- /dev/null
+++ b/src/mbgl/annotation/sprite_parser.hpp
@@ -0,0 +1,39 @@
+#ifndef MBGL_ANNOTATIONS_SPRITE_PARSER
+#define MBGL_ANNOTATIONS_SPRITE_PARSER
+
+#include <mbgl/util/noncopyable.hpp>
+#include <mbgl/util/geo.hpp>
+
+#include <string>
+#include <memory>
+#include <map>
+
+namespace mbgl {
+
+namespace util {
+
+class Image;
+
+} // namespace util
+
+class SpriteImage;
+
+using SpriteImagePtr = std::shared_ptr<const SpriteImage>;
+
+// Extracts an individual image from a spritesheet from the given location.
+SpriteImagePtr createSpriteImage(const util::Image& image,
+ uint16_t srcX,
+ uint16_t srcY,
+ uint16_t srcWidth,
+ uint16_t srcHeight,
+ double ratio,
+ bool sdf);
+
+using Sprites = std::map<std::string, SpriteImagePtr>;
+
+// Parses an image and an associated JSON file and returns the sprite objects.
+Sprites parseSprite(const std::string& image, const std::string& json);
+
+} // namespace mbgl
+
+#endif
diff --git a/test/annotations/sprite_parser.cpp b/test/annotations/sprite_parser.cpp
new file mode 100644
index 0000000000..e8dcfd70a5
--- /dev/null
+++ b/test/annotations/sprite_parser.cpp
@@ -0,0 +1,309 @@
+#include "../fixtures/util.hpp"
+#include "../fixtures/fixture_log_observer.hpp"
+
+#include <mbgl/annotation/sprite_parser.hpp>
+#include <mbgl/annotation/sprite_image.hpp>
+#include <mbgl/util/image.hpp>
+#include <mbgl/util/io.hpp>
+
+using namespace mbgl;
+
+TEST(Annotations, SpriteImageCreationInvalid) {
+ const util::Image image_1x = { util::read_file("test/fixtures/annotations/emerald.png") };
+ ASSERT_TRUE(image_1x);
+ ASSERT_EQ(200u, image_1x.getWidth());
+ ASSERT_EQ(299u, image_1x.getHeight());
+
+ // invalid dimensions
+ ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 0, 16, 1, false)); // width == 0
+ ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 16, 0, 1, false)); // height == 0
+ ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 1, 1, 0, false)); // ratio == 0
+ ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 1, 1, 23, false)); // ratio too large
+ ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 2048, 16, 1, false)); // too wide
+ ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 16, 1025, 1, false)); // too tall
+}
+
+TEST(Annotations, SpriteImageCreation1x) {
+ const util::Image image_1x = { util::read_file("test/fixtures/annotations/emerald.png") };
+ ASSERT_TRUE(image_1x);
+ ASSERT_EQ(200u, image_1x.getWidth());
+ ASSERT_EQ(299u, image_1x.getHeight());
+
+ { // "museum_icon":{"x":177,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false}
+ const auto sprite = createSpriteImage(image_1x, 177, 187, 18, 18, 1, false);
+ ASSERT_TRUE(sprite.get());
+ EXPECT_EQ(18, sprite->width);
+ EXPECT_EQ(18, sprite->height);
+ EXPECT_EQ(18, sprite->pixelWidth);
+ EXPECT_EQ(18, sprite->pixelHeight);
+ EXPECT_EQ(1, sprite->pixelRatio);
+ EXPECT_EQ(0xC83FE8FA9665D177u, std::hash<std::string>()(sprite->data));
+ }
+
+ { // outside image == blank
+ const auto sprite = createSpriteImage(image_1x, 200, 0, 16, 16, 1, false);
+ ASSERT_TRUE(sprite.get());
+ EXPECT_EQ(16, sprite->width);
+ EXPECT_EQ(16, sprite->height);
+ EXPECT_EQ(16, sprite->pixelWidth);
+ EXPECT_EQ(16, sprite->pixelHeight);
+ EXPECT_EQ(1, sprite->pixelRatio);
+ EXPECT_EQ(0x5599CFD89CB402A6u, std::hash<std::string>()(sprite->data));
+ }
+
+ { // outside image == blank
+ const auto sprite = createSpriteImage(image_1x, 0, 300, 16, 16, 1, false);
+ ASSERT_TRUE(sprite.get());
+ EXPECT_EQ(16, sprite->width);
+ EXPECT_EQ(16, sprite->height);
+ EXPECT_EQ(16, sprite->pixelWidth);
+ EXPECT_EQ(16, sprite->pixelHeight);
+ EXPECT_EQ(1, sprite->pixelRatio);
+ EXPECT_EQ(0x5599CFD89CB402A6u, std::hash<std::string>()(sprite->data));
+ }
+}
+
+TEST(Annotations, SpriteImageCreation2x) {
+ const util::Image image_2x = { util::read_file("test/fixtures/annotations/emerald@2x.png") };
+ ASSERT_TRUE(image_2x);
+
+ // "museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false}
+ const auto sprite = createSpriteImage(image_2x, 354, 374, 36, 36, 2, false);
+ ASSERT_TRUE(sprite.get());
+ EXPECT_EQ(18, sprite->width);
+ EXPECT_EQ(18, sprite->height);
+ EXPECT_EQ(36, sprite->pixelWidth);
+ EXPECT_EQ(36, sprite->pixelHeight);
+ EXPECT_EQ(2, sprite->pixelRatio);
+ EXPECT_EQ(0x2446A6D2C350B6AEu, std::hash<std::string>()(sprite->data));
+}
+
+TEST(Annotations, SpriteImageCreation1_5x) {
+ const util::Image image_2x = { util::read_file("test/fixtures/annotations/emerald@2x.png") };
+ ASSERT_TRUE(image_2x);
+
+ // "museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false}
+ const auto sprite = createSpriteImage(image_2x, 354, 374, 36, 36, 1.5, false);
+ ASSERT_TRUE(sprite.get());
+ EXPECT_EQ(24, sprite->width);
+ EXPECT_EQ(24, sprite->height);
+ EXPECT_EQ(36, sprite->pixelWidth);
+ EXPECT_EQ(36, sprite->pixelHeight);
+ EXPECT_EQ(1.5, sprite->pixelRatio);
+ EXPECT_EQ(0x2446A6D2C350B6AEu, std::hash<std::string>()(sprite->data));
+
+ // "hospital_icon":{"x":314,"y":518,"width":36,"height":36,"pixelRatio":2,"sdf":false}
+ const auto sprite2 = createSpriteImage(image_2x, 314, 518, 35, 35, 1.5, false);
+ ASSERT_TRUE(sprite2.get());
+ EXPECT_EQ(24, sprite2->width);
+ EXPECT_EQ(24, sprite2->height);
+ EXPECT_EQ(36, sprite2->pixelWidth);
+ EXPECT_EQ(36, sprite2->pixelHeight);
+ EXPECT_EQ(1.5, sprite2->pixelRatio);
+ EXPECT_EQ(0xF5274FF7FABA1C8Du, std::hash<std::string>()(sprite2->data));
+}
+
+TEST(Annotations, SpriteParsing) {
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = util::read_file("test/fixtures/annotations/emerald.json");
+
+ const auto images = parseSprite(image_1x, json_1x);
+
+ std::set<std::string> names;
+ std::transform(images.begin(), images.end(), std::inserter(names, names.begin()),
+ [](const auto& pair) { return pair.first; });
+
+ EXPECT_EQ(std::set<std::string>({ "airfield_icon",
+ "airport_icon",
+ "background",
+ "cemetery_icon",
+ "college_icon",
+ "default_1",
+ "default_2",
+ "default_3",
+ "default_4",
+ "default_5",
+ "default_6",
+ "default_marker",
+ "dlr",
+ "dlr.london-overground.london-underground.national-rail",
+ "dlr.london-underground",
+ "dlr.london-underground.national-rail",
+ "dlr.national-rail",
+ "dot",
+ "embassy_icon",
+ "fire-station_icon",
+ "generic-metro",
+ "generic-rail",
+ "generic_icon",
+ "golf_icon",
+ "government_icon",
+ "grass_pattern",
+ "harbor_icon",
+ "hospital_icon",
+ "hospital_striped",
+ "interstate_1",
+ "interstate_2",
+ "interstate_3",
+ "library_icon",
+ "london-overground",
+ "london-overground.london-underground",
+ "london-overground.london-underground.national-rail",
+ "london-overground.national-rail",
+ "london-underground",
+ "london-underground.national-rail",
+ "marker_icon",
+ "metro",
+ "metro.rer",
+ "monument_icon",
+ "moscow-metro",
+ "museum_icon",
+ "national-rail",
+ "oneway_motorway",
+ "oneway_road",
+ "park_icon",
+ "police_icon",
+ "post_icon",
+ "prison_icon",
+ "religious-christian_icon",
+ "religious-jewish_icon",
+ "religious-muslim_icon",
+ "rer",
+ "rer.transilien",
+ "s-bahn",
+ "s-bahn.u-bahn",
+ "sand_noise",
+ "school_icon",
+ "school_striped",
+ "secondary_marker",
+ "u-bahn",
+ "us_highway_1",
+ "us_highway_2",
+ "us_highway_3",
+ "us_state_1",
+ "us_state_2",
+ "us_state_3",
+ "washington-metro",
+ "wiener-linien",
+ "zoo_icon" }),
+ names);
+
+ {
+ auto sprite = images.find("generic-metro")->second;
+ EXPECT_EQ(18, sprite->width);
+ EXPECT_EQ(18, sprite->height);
+ EXPECT_EQ(18, sprite->pixelWidth);
+ EXPECT_EQ(18, sprite->pixelHeight);
+ EXPECT_EQ(1, sprite->pixelRatio);
+ EXPECT_EQ(0X4829734034980451u, std::hash<std::string>()(sprite->data));
+ }
+}
+
+TEST(Annotations, SpriteParsingInvalidJSON) {
+ FixtureLog log;
+
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = R"JSON({ "image": " })JSON";
+
+ const auto images = parseSprite(image_1x, json_1x);
+ EXPECT_EQ(0u, images.size());
+
+ EXPECT_EQ(
+ 1u,
+ log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Failed to parse JSON: lacks ending quotation before the end of string at offset 10",
+ }));
+}
+
+TEST(Annotations, SpriteParsingEmptyImage) {
+ FixtureLog log;
+
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = R"JSON({ "image": {} })JSON";
+
+ const auto images = parseSprite(image_1x, json_1x);
+ EXPECT_EQ(0u, images.size());
+
+ EXPECT_EQ(1u, log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Can't create sprite with invalid metrics",
+ }));
+}
+
+TEST(Annotations, SpriteParsingSimpleWidthHeight) {
+ FixtureLog log;
+
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = R"JSON({ "image": { "width": 32, "height": 32 } })JSON";
+
+ const auto images = parseSprite(image_1x, json_1x);
+ EXPECT_EQ(1u, images.size());
+}
+
+TEST(Annotations, SpriteParsingWidthTooBig) {
+ FixtureLog log;
+
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = R"JSON({ "image": { "width": 65536, "height": 32 } })JSON";
+
+ const auto images = parseSprite(image_1x, json_1x);
+ EXPECT_EQ(0u, images.size());
+
+ EXPECT_EQ(1u, log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Value of 'width' must be an integer between 0 and 65535",
+ }));
+ EXPECT_EQ(1u, log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Can't create sprite with invalid metrics",
+ }));
+}
+
+TEST(Annotations, SpriteParsingNegativeWidth) {
+ FixtureLog log;
+
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = R"JSON({ "image": { "width": -1, "height": 32 } })JSON";
+
+ const auto images = parseSprite(image_1x, json_1x);
+ EXPECT_EQ(0u, images.size());
+
+ EXPECT_EQ(1u, log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Value of 'width' must be an integer between 0 and 65535",
+ }));
+ EXPECT_EQ(1u, log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Can't create sprite with invalid metrics",
+ }));
+}
+
+TEST(Annotations, SpriteParsingNullRatio) {
+ FixtureLog log;
+
+ const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png");
+ const auto json_1x = R"JSON({ "image": { "width": 32, "height": 32, "pixelRatio": 0 } })JSON";
+
+ const auto images = parseSprite(image_1x, json_1x);
+ EXPECT_EQ(0u, images.size());
+
+ EXPECT_EQ(1u, log.count({
+ EventSeverity::Warning,
+ Event::Sprite,
+ int64_t(-1),
+ "Can't create sprite with invalid metrics",
+ }));
+}
diff --git a/test/fixtures/annotations/emerald.json b/test/fixtures/annotations/emerald.json
new file mode 100644
index 0000000000..dcc2b4808c
--- /dev/null
+++ b/test/fixtures/annotations/emerald.json
@@ -0,0 +1 @@
+{"background":{"x":0,"y":20,"width":50,"height":50,"pixelRatio":1,"sdf":false},"grass_pattern":{"x":100,"y":80,"width":50,"height":50,"pixelRatio":1,"sdf":false},"interstate_1":{"x":0,"y":100,"width":41,"height":40,"pixelRatio":1,"sdf":false},"interstate_2":{"x":0,"y":100,"width":41,"height":40,"pixelRatio":1,"sdf":false},"interstate_3":{"x":41,"y":100,"width":48,"height":39,"pixelRatio":1,"sdf":false},"us_state_1":{"x":0,"y":73,"width":29,"height":24,"pixelRatio":1,"sdf":false},"us_state_2":{"x":0,"y":73,"width":29,"height":24,"pixelRatio":1,"sdf":false},"us_state_3":{"x":30,"y":73,"width":32,"height":24,"pixelRatio":1,"sdf":false},"us_highway_1":{"x":0,"y":142,"width":29,"height":29,"pixelRatio":1,"sdf":false},"us_highway_2":{"x":30,"y":142,"width":33,"height":29,"pixelRatio":1,"sdf":false},"us_highway_3":{"x":64,"y":142,"width":36,"height":29,"pixelRatio":1,"sdf":false},"default_1":{"x":0,"y":0,"width":17,"height":16,"pixelRatio":1,"sdf":false},"default_2":{"x":17,"y":0,"width":22,"height":16,"pixelRatio":1,"sdf":false},"default_3":{"x":39,"y":0,"width":27,"height":16,"pixelRatio":1,"sdf":false},"default_4":{"x":66,"y":0,"width":32,"height":16,"pixelRatio":1,"sdf":false},"default_5":{"x":98,"y":0,"width":37,"height":16,"pixelRatio":1,"sdf":false},"default_6":{"x":135,"y":0,"width":42,"height":16,"pixelRatio":1,"sdf":false},"london-overground":{"x":70,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"london-underground":{"x":88,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"national-rail":{"x":106,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"dlr":{"x":106,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"dlr.london-overground.london-underground.national-rail":{"x":70,"y":25,"width":72,"height":18,"pixelRatio":1,"sdf":false},"dlr.london-underground":{"x":88,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"dlr.london-underground.national-rail":{"x":88,"y":25,"width":54,"height":18,"pixelRatio":1,"sdf":false},"dlr.national-rail":{"x":106,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"london-overground.london-underground":{"x":70,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"london-overground.london-underground.national-rail":{"x":124,"y":25,"width":54,"height":18,"pixelRatio":1,"sdf":false},"london-overground.national-rail":{"x":124,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"london-underground.national-rail":{"x":124,"y":43,"width":36,"height":18,"pixelRatio":1,"sdf":false},"metro":{"x":71,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"rer":{"x":87,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"metro.rer":{"x":71,"y":43,"width":34,"height":18,"pixelRatio":1,"sdf":false},"rer.transilien":{"x":87,"y":43,"width":36,"height":18,"pixelRatio":1,"sdf":false},"u-bahn":{"x":70,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"s-bahn":{"x":88,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"s-bahn.u-bahn":{"x":70,"y":62,"width":36,"height":18,"pixelRatio":1,"sdf":false},"washington-metro":{"x":106,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"wiener-linien":{"x":124,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"moscow-metro":{"x":142,"y":61,"width":21,"height":18,"pixelRatio":1,"sdf":false},"generic-metro":{"x":160,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"generic-rail":{"x":178,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"dot":{"x":166,"y":63,"width":13,"height":13,"pixelRatio":1,"sdf":false},"default_marker":{"x":0,"y":175,"width":33,"height":86,"pixelRatio":1,"sdf":false},"secondary_marker":{"x":33,"y":175,"width":33,"height":86,"pixelRatio":1,"sdf":false},"oneway_motorway":{"x":178,"y":24,"width":21,"height":19,"pixelRatio":1,"sdf":false},"oneway_road":{"x":178,"y":62,"width":21,"height":19,"pixelRatio":1,"sdf":false},"hospital_icon":{"x":157,"y":259,"width":18,"height":18,"pixelRatio":1,"sdf":false},"fire-station_icon":{"x":157,"y":241,"width":18,"height":18,"pixelRatio":1,"sdf":false},"cemetery_icon":{"x":157,"y":79,"width":18,"height":18,"pixelRatio":1,"sdf":false},"zoo_icon":{"x":177,"y":79,"width":18,"height":18,"pixelRatio":1,"sdf":false},"park_icon":{"x":177,"y":97,"width":18,"height":18,"pixelRatio":1,"sdf":false},"golf_icon":{"x":177,"y":115,"width":18,"height":18,"pixelRatio":1,"sdf":false},"school_icon":{"x":177,"y":133,"width":18,"height":18,"pixelRatio":1,"sdf":false},"monument_icon":{"x":177,"y":151,"width":18,"height":18,"pixelRatio":1,"sdf":false},"library_icon":{"x":177,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"museum_icon":{"x":177,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"college_icon":{"x":177,"y":205,"width":18,"height":18,"pixelRatio":1,"sdf":false},"religious-christian_icon":{"x":157,"y":115,"width":18,"height":18,"pixelRatio":1,"sdf":false},"religious-jewish_icon":{"x":157,"y":133,"width":18,"height":18,"pixelRatio":1,"sdf":false},"religious-muslim_icon":{"x":157,"y":151,"width":18,"height":18,"pixelRatio":1,"sdf":false},"government_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"post_icon":{"x":157,"y":205,"width":18,"height":18,"pixelRatio":1,"sdf":false},"embassy_icon":{"x":157,"y":223,"width":18,"height":18,"pixelRatio":1,"sdf":false},"police_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"marker_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"prison_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"airfield_icon":{"x":157,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"airport_icon":{"x":157,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"harbor_icon":{"x":139,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"generic_icon":{"x":139,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"hospital_striped":{"x":117,"y":135,"width":3,"height":3,"pixelRatio":1,"sdf":false},"school_striped":{"x":114,"y":135,"width":3,"height":3,"pixelRatio":1,"sdf":false},"sand_noise":{"x":75,"y":174,"width":50,"height":50,"pixelRatio":1,"sdf":false}} \ No newline at end of file
diff --git a/test/fixtures/annotations/emerald.png b/test/fixtures/annotations/emerald.png
new file mode 100644
index 0000000000..967f2e76a6
--- /dev/null
+++ b/test/fixtures/annotations/emerald.png
Binary files differ
diff --git a/test/fixtures/annotations/emerald@2x.json b/test/fixtures/annotations/emerald@2x.json
new file mode 100644
index 0000000000..250aa36194
--- /dev/null
+++ b/test/fixtures/annotations/emerald@2x.json
@@ -0,0 +1 @@
+{"background":{"x":0,"y":40,"width":100,"height":100,"pixelRatio":2,"sdf":false},"grass_pattern":{"x":200,"y":160,"width":100,"height":100,"pixelRatio":2,"sdf":false},"interstate_1":{"x":0,"y":200,"width":82,"height":80,"pixelRatio":2,"sdf":false},"interstate_2":{"x":0,"y":200,"width":82,"height":80,"pixelRatio":2,"sdf":false},"interstate_3":{"x":82,"y":200,"width":96,"height":78,"pixelRatio":2,"sdf":false},"us_state_1":{"x":0,"y":146,"width":58,"height":48,"pixelRatio":2,"sdf":false},"us_state_2":{"x":0,"y":146,"width":58,"height":48,"pixelRatio":2,"sdf":false},"us_state_3":{"x":60,"y":146,"width":64,"height":48,"pixelRatio":2,"sdf":false},"us_highway_1":{"x":0,"y":284,"width":58,"height":58,"pixelRatio":2,"sdf":false},"us_highway_2":{"x":60,"y":284,"width":66,"height":58,"pixelRatio":2,"sdf":false},"us_highway_3":{"x":128,"y":284,"width":72,"height":58,"pixelRatio":2,"sdf":false},"default_1":{"x":0,"y":0,"width":34,"height":32,"pixelRatio":2,"sdf":false},"default_2":{"x":34,"y":0,"width":44,"height":32,"pixelRatio":2,"sdf":false},"default_3":{"x":78,"y":0,"width":54,"height":32,"pixelRatio":2,"sdf":false},"default_4":{"x":132,"y":0,"width":64,"height":32,"pixelRatio":2,"sdf":false},"default_5":{"x":196,"y":0,"width":74,"height":32,"pixelRatio":2,"sdf":false},"default_6":{"x":270,"y":0,"width":84,"height":32,"pixelRatio":2,"sdf":false},"london-overground":{"x":140,"y":50,"width":36,"height":36,"pixelRatio":2,"sdf":false},"london-underground":{"x":176,"y":50,"width":36,"height":36,"pixelRatio":2,"sdf":false},"national-rail":{"x":212,"y":50,"width":36,"height":36,"pixelRatio":2,"sdf":false},"dlr":{"x":212,"y":50,"width":36,"height":36,"pixelRatio":2,"sdf":false},"dlr.london-overground.london-underground.national-rail":{"x":140,"y":50,"width":144,"height":36,"pixelRatio":2,"sdf":false},"dlr.london-underground":{"x":176,"y":50,"width":72,"height":36,"pixelRatio":2,"sdf":false},"dlr.london-underground.national-rail":{"x":176,"y":50,"width":108,"height":36,"pixelRatio":2,"sdf":false},"dlr.national-rail":{"x":212,"y":50,"width":72,"height":36,"pixelRatio":2,"sdf":false},"london-overground.london-underground":{"x":140,"y":50,"width":72,"height":36,"pixelRatio":2,"sdf":false},"london-overground.london-underground.national-rail":{"x":248,"y":50,"width":108,"height":36,"pixelRatio":2,"sdf":false},"london-overground.national-rail":{"x":248,"y":50,"width":72,"height":36,"pixelRatio":2,"sdf":false},"london-underground.national-rail":{"x":248,"y":86,"width":72,"height":36,"pixelRatio":2,"sdf":false},"metro":{"x":142,"y":86,"width":36,"height":36,"pixelRatio":2,"sdf":false},"rer":{"x":174,"y":86,"width":36,"height":36,"pixelRatio":2,"sdf":false},"metro.rer":{"x":142,"y":86,"width":68,"height":36,"pixelRatio":2,"sdf":false},"rer.transilien":{"x":174,"y":86,"width":72,"height":36,"pixelRatio":2,"sdf":false},"u-bahn":{"x":140,"y":124,"width":36,"height":36,"pixelRatio":2,"sdf":false},"s-bahn":{"x":176,"y":124,"width":36,"height":36,"pixelRatio":2,"sdf":false},"s-bahn.u-bahn":{"x":140,"y":124,"width":72,"height":36,"pixelRatio":2,"sdf":false},"washington-metro":{"x":212,"y":124,"width":36,"height":36,"pixelRatio":2,"sdf":false},"wiener-linien":{"x":248,"y":124,"width":36,"height":36,"pixelRatio":2,"sdf":false},"moscow-metro":{"x":284,"y":122,"width":42,"height":36,"pixelRatio":2,"sdf":false},"generic-metro":{"x":320,"y":86,"width":36,"height":36,"pixelRatio":2,"sdf":false},"generic-rail":{"x":356,"y":86,"width":36,"height":36,"pixelRatio":2,"sdf":false},"dot":{"x":332,"y":126,"width":26,"height":26,"pixelRatio":2,"sdf":false},"default_marker":{"x":0,"y":350,"width":66,"height":172,"pixelRatio":2,"sdf":false},"secondary_marker":{"x":66,"y":350,"width":66,"height":172,"pixelRatio":2,"sdf":false},"oneway_motorway":{"x":356,"y":48,"width":42,"height":38,"pixelRatio":2,"sdf":false},"oneway_road":{"x":356,"y":124,"width":42,"height":38,"pixelRatio":2,"sdf":false},"hospital_icon":{"x":314,"y":518,"width":36,"height":36,"pixelRatio":2,"sdf":false},"fire-station_icon":{"x":314,"y":482,"width":36,"height":36,"pixelRatio":2,"sdf":false},"cemetery_icon":{"x":314,"y":158,"width":36,"height":36,"pixelRatio":2,"sdf":false},"zoo_icon":{"x":354,"y":158,"width":36,"height":36,"pixelRatio":2,"sdf":false},"park_icon":{"x":354,"y":194,"width":36,"height":36,"pixelRatio":2,"sdf":false},"golf_icon":{"x":354,"y":230,"width":36,"height":36,"pixelRatio":2,"sdf":false},"school_icon":{"x":354,"y":266,"width":36,"height":36,"pixelRatio":2,"sdf":false},"monument_icon":{"x":354,"y":302,"width":36,"height":36,"pixelRatio":2,"sdf":false},"library_icon":{"x":354,"y":338,"width":36,"height":36,"pixelRatio":2,"sdf":false},"museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false},"college_icon":{"x":354,"y":410,"width":36,"height":36,"pixelRatio":2,"sdf":false},"religious-christian_icon":{"x":314,"y":230,"width":36,"height":36,"pixelRatio":2,"sdf":false},"religious-jewish_icon":{"x":314,"y":266,"width":36,"height":36,"pixelRatio":2,"sdf":false},"religious-muslim_icon":{"x":314,"y":302,"width":36,"height":36,"pixelRatio":2,"sdf":false},"government_icon":{"x":314,"y":338,"width":36,"height":36,"pixelRatio":2,"sdf":false},"post_icon":{"x":314,"y":410,"width":36,"height":36,"pixelRatio":2,"sdf":false},"embassy_icon":{"x":314,"y":446,"width":36,"height":36,"pixelRatio":2,"sdf":false},"police_icon":{"x":314,"y":338,"width":36,"height":36,"pixelRatio":2,"sdf":false},"marker_icon":{"x":314,"y":338,"width":36,"height":36,"pixelRatio":2,"sdf":false},"prison_icon":{"x":314,"y":338,"width":36,"height":36,"pixelRatio":2,"sdf":false},"airfield_icon":{"x":314,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false},"airport_icon":{"x":314,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false},"harbor_icon":{"x":278,"y":338,"width":36,"height":36,"pixelRatio":2,"sdf":false},"generic_icon":{"x":278,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false},"hospital_striped":{"x":234,"y":270,"width":6,"height":6,"pixelRatio":2,"sdf":false},"school_striped":{"x":228,"y":270,"width":6,"height":6,"pixelRatio":2,"sdf":false},"sand_noise":{"x":150,"y":348,"width":100,"height":100,"pixelRatio":2,"sdf":false}} \ No newline at end of file
diff --git a/test/fixtures/annotations/emerald@2x.png b/test/fixtures/annotations/emerald@2x.png
new file mode 100644
index 0000000000..a1ffbd95ea
--- /dev/null
+++ b/test/fixtures/annotations/emerald@2x.png
Binary files differ
diff --git a/test/test.gypi b/test/test.gypi
index 588c61f59a..93f1cc81ed 100644
--- a/test/test.gypi
+++ b/test/test.gypi
@@ -39,6 +39,7 @@
'annotations/sprite_image.cpp',
'annotations/sprite_store.cpp',
+ 'annotations/sprite_parser.cpp',
'api/api_misuse.cpp',
'api/repeated_render.cpp',