diff options
author | Chris Loer <chris.loer@gmail.com> | 2017-11-21 15:12:07 -0800 |
---|---|---|
committer | Chris Loer <chris.loer@mapbox.com> | 2017-12-11 10:43:00 -0800 |
commit | 1661af1cb409505bb362e2274cb61f097ad25864 (patch) | |
tree | 184b9000d4b5a2f8760468eaa018adc9d6de24ae | |
parent | 730054276ed6a5dc7ba9d19765b8457a818854f7 (diff) | |
download | qtlocation-mapboxgl-1661af1cb409505bb362e2274cb61f097ad25864.tar.gz |
[core] Enable local glyph generation using TinySDF.
- Platform-specific LocalGlyphRasterizer is responsible for deciding which glyphs to rasterize locally and for implementing the rasterization.
- Default platform implementation doesn't locally generate any glyphs -> no behavior change
- Unit test uses StubLocalGlyphRasterizer, which returns a single fixed bitmap for all CJK glyphs
- Rename glyph_loader.test to glyph_manager.test
-rw-r--r-- | cmake/core-files.cmake | 1 | ||||
-rw-r--r-- | cmake/test-files.cmake | 2 | ||||
-rw-r--r-- | platform/android/config.cmake | 1 | ||||
-rw-r--r-- | platform/default/local_glyph_rasterizer.cpp | 13 | ||||
-rw-r--r-- | platform/ios/config.cmake | 1 | ||||
-rw-r--r-- | platform/linux/config.cmake | 1 | ||||
-rw-r--r-- | platform/macos/config.cmake | 1 | ||||
-rw-r--r-- | platform/qt/config.cmake | 2 | ||||
-rw-r--r-- | src/mbgl/text/glyph_manager.cpp | 20 | ||||
-rw-r--r-- | src/mbgl/text/glyph_manager.hpp | 9 | ||||
-rw-r--r-- | src/mbgl/text/local_glyph_rasterizer.hpp | 42 | ||||
-rw-r--r-- | test/text/glyph_loader.test.cpp | 265 | ||||
-rw-r--r-- | test/text/glyph_manager.test.cpp | 341 |
13 files changed, 428 insertions, 271 deletions
diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake index 138fea1bcb..15812da42d 100644 --- a/cmake/core-files.cmake +++ b/cmake/core-files.cmake @@ -554,6 +554,7 @@ set(MBGL_CORE_FILES src/mbgl/text/glyph_pbf.cpp src/mbgl/text/glyph_pbf.hpp src/mbgl/text/glyph_range.hpp + src/mbgl/text/local_glyph_rasterizer.hpp src/mbgl/text/placement.cpp src/mbgl/text/placement.hpp src/mbgl/text/quads.cpp diff --git a/cmake/test-files.cmake b/cmake/test-files.cmake index 273852602f..1438e463d5 100644 --- a/cmake/test-files.cmake +++ b/cmake/test-files.cmake @@ -113,7 +113,7 @@ set(MBGL_TEST_FILES # text test/text/cross_tile_symbol_index.test.cpp - test/text/glyph_loader.test.cpp + test/text/glyph_manager.test.cpp test/text/glyph_pbf.test.cpp test/text/quads.test.cpp diff --git a/platform/android/config.cmake b/platform/android/config.cmake index 96ec901a42..a915f1d93d 100644 --- a/platform/android/config.cmake +++ b/platform/android/config.cmake @@ -41,6 +41,7 @@ macro(mbgl_platform_core) PRIVATE platform/android/src/thread.cpp PRIVATE platform/default/string_stdlib.cpp PRIVATE platform/default/bidi.cpp + PRIVATE platform/default/local_glyph_rasterizer.cpp PRIVATE platform/default/thread_local.cpp PRIVATE platform/default/utf.cpp diff --git a/platform/default/local_glyph_rasterizer.cpp b/platform/default/local_glyph_rasterizer.cpp new file mode 100644 index 0000000000..7ace6cbfb1 --- /dev/null +++ b/platform/default/local_glyph_rasterizer.cpp @@ -0,0 +1,13 @@ +#include <mbgl/text/local_glyph_rasterizer.hpp> + +namespace mbgl { + +bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack&, GlyphID) { + return false; +} + +Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack&, GlyphID) { + return Glyph(); +} + +} // namespace mbgl diff --git a/platform/ios/config.cmake b/platform/ios/config.cmake index 7f54d71313..47665c33f1 100644 --- a/platform/ios/config.cmake +++ b/platform/ios/config.cmake @@ -32,6 +32,7 @@ macro(mbgl_platform_core) PRIVATE platform/darwin/src/nsthread.mm PRIVATE platform/darwin/src/string_nsstring.mm PRIVATE platform/default/bidi.cpp + PRIVATE platform/default/local_glyph_rasterizer.cpp PRIVATE platform/default/thread_local.cpp PRIVATE platform/default/utf.cpp diff --git a/platform/linux/config.cmake b/platform/linux/config.cmake index fef0f4486c..6027b1cd55 100644 --- a/platform/linux/config.cmake +++ b/platform/linux/config.cmake @@ -47,6 +47,7 @@ macro(mbgl_platform_core) PRIVATE platform/default/string_stdlib.cpp PRIVATE platform/default/thread.cpp PRIVATE platform/default/bidi.cpp + PRIVATE platform/default/local_glyph_rasterizer.cpp PRIVATE platform/default/thread_local.cpp PRIVATE platform/default/utf.cpp diff --git a/platform/macos/config.cmake b/platform/macos/config.cmake index 45cae9bf7d..7c52e7d537 100644 --- a/platform/macos/config.cmake +++ b/platform/macos/config.cmake @@ -18,6 +18,7 @@ macro(mbgl_platform_core) PRIVATE platform/darwin/src/nsthread.mm PRIVATE platform/darwin/src/string_nsstring.mm PRIVATE platform/default/bidi.cpp + PRIVATE platform/default/local_glyph_rasterizer.cpp PRIVATE platform/default/thread_local.cpp PRIVATE platform/default/utf.cpp diff --git a/platform/qt/config.cmake b/platform/qt/config.cmake index e3b15fddfd..9417963767 100644 --- a/platform/qt/config.cmake +++ b/platform/qt/config.cmake @@ -49,6 +49,8 @@ macro(mbgl_platform_core) target_sources(mbgl-core PRIVATE platform/qt/src/bidi.cpp) endif() + target_sources(mbgl-core PRIVATE platform/default/local_glyph_rasterizer.cpp) + endmacro() diff --git a/src/mbgl/text/glyph_manager.cpp b/src/mbgl/text/glyph_manager.cpp index c79a1938c1..3130418908 100644 --- a/src/mbgl/text/glyph_manager.cpp +++ b/src/mbgl/text/glyph_manager.cpp @@ -4,14 +4,16 @@ #include <mbgl/storage/file_source.hpp> #include <mbgl/storage/resource.hpp> #include <mbgl/storage/response.hpp> +#include <mbgl/util/tiny_sdf.hpp> namespace mbgl { static GlyphManagerObserver nullObserver; -GlyphManager::GlyphManager(FileSource& fileSource_) +GlyphManager::GlyphManager(FileSource& fileSource_, std::unique_ptr<LocalGlyphRasterizer> localGlyphRasterizer_) : fileSource(fileSource_), - observer(&nullObserver) { + observer(&nullObserver), + localGlyphRasterizer(std::move(localGlyphRasterizer_)) { } GlyphManager::~GlyphManager() = default; @@ -30,7 +32,13 @@ void GlyphManager::getGlyphs(GlyphRequestor& requestor, GlyphDependencies glyphD const GlyphIDs& glyphIDs = dependency.second; GlyphRangeSet ranges; for (const auto& glyphID : glyphIDs) { - ranges.insert(getGlyphRange(glyphID)); + if (localGlyphRasterizer->canRasterizeGlyph(fontStack, glyphID)) { + if (entry.glyphs.find(glyphID) == entry.glyphs.end()) { + entry.glyphs.emplace(glyphID, makeMutable<Glyph>(generateLocalSDF(fontStack, glyphID))); + } + } else { + ranges.insert(getGlyphRange(glyphID)); + } } for (const auto& range : ranges) { @@ -50,6 +58,12 @@ void GlyphManager::getGlyphs(GlyphRequestor& requestor, GlyphDependencies glyphD } } +Glyph GlyphManager::generateLocalSDF(const FontStack& fontStack, GlyphID glyphID) { + Glyph local = localGlyphRasterizer->rasterizeGlyph(fontStack, glyphID); + local.bitmap = util::transformRasterToSDF(local.bitmap, 8, .25); + return local; +} + void GlyphManager::requestRange(GlyphRequest& request, const FontStack& fontStack, const GlyphRange& range) { if (request.req) { return; diff --git a/src/mbgl/text/glyph_manager.hpp b/src/mbgl/text/glyph_manager.hpp index de2b9cde7b..194f503ff1 100644 --- a/src/mbgl/text/glyph_manager.hpp +++ b/src/mbgl/text/glyph_manager.hpp @@ -3,6 +3,7 @@ #include <mbgl/text/glyph.hpp> #include <mbgl/text/glyph_manager_observer.hpp> #include <mbgl/text/glyph_range.hpp> +#include <mbgl/text/local_glyph_rasterizer.hpp> #include <mbgl/util/noncopyable.hpp> #include <mbgl/util/font_stack.hpp> #include <mbgl/util/immutable.hpp> @@ -24,7 +25,7 @@ public: class GlyphManager : public util::noncopyable { public: - GlyphManager(FileSource&); + GlyphManager(FileSource&, std::unique_ptr<LocalGlyphRasterizer> = std::make_unique<LocalGlyphRasterizer>()); ~GlyphManager(); // Workers send a `getGlyphs` message to the main thread once they have determined @@ -42,6 +43,8 @@ public: void setObserver(GlyphManagerObserver*); private: + Glyph generateLocalSDF(const FontStack& fontStack, GlyphID glyphID); + FileSource& fileSource; std::string glyphURL; @@ -61,8 +64,10 @@ private: void requestRange(GlyphRequest&, const FontStack&, const GlyphRange&); void processResponse(const Response&, const FontStack&, const GlyphRange&); void notify(GlyphRequestor&, const GlyphDependencies&); - + GlyphManagerObserver* observer = nullptr; + + std::unique_ptr<LocalGlyphRasterizer> localGlyphRasterizer; }; } // namespace mbgl diff --git a/src/mbgl/text/local_glyph_rasterizer.hpp b/src/mbgl/text/local_glyph_rasterizer.hpp new file mode 100644 index 0000000000..c2bdbd2840 --- /dev/null +++ b/src/mbgl/text/local_glyph_rasterizer.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <mbgl/text/glyph.hpp> + +namespace mbgl { + +/* + Given a font stack and a glyph ID, platform-specific implementations of + LocalGlyphRasterizer will decide which, if any, local fonts to use and + then generate a matching glyph object with a greyscale rasterization of + the glyph and appropriate metrics. GlyphManager will then use TinySDF to + transform the rasterized bitmap into an SDF. + + The JS equivalent of this functionality will only generate glyphs in the + 'CJK Unified Ideographs' and 'Hangul Syllables' ranges, for which it can + get away with rendering a fixed 30px square image and GlyphMetrics of: + + width: 24, + height: 24, + left: 0, + top: -8, + advance: 24 + + The JS equivalent also uses heuristic evaluation of the font stack name + to control the font-weight it uses during rasterization. + + It is left to platform-specific implementation to decide how best to + map a FontStack to a particular rasterization. + + The default implementation simply refuses to rasterize any glyphs. +*/ + +class LocalGlyphRasterizer { +public: + virtual ~LocalGlyphRasterizer() = default; + + // virtual so that test harness can override platform-specific behavior + virtual bool canRasterizeGlyph(const FontStack&, GlyphID); + virtual Glyph rasterizeGlyph(const FontStack&, GlyphID); +}; + +} // namespace mbgl diff --git a/test/text/glyph_loader.test.cpp b/test/text/glyph_loader.test.cpp deleted file mode 100644 index 20ac045925..0000000000 --- a/test/text/glyph_loader.test.cpp +++ /dev/null @@ -1,265 +0,0 @@ -#include <mbgl/test/util.hpp> -#include <mbgl/test/stub_file_source.hpp> - -#include <mbgl/text/glyph_manager.hpp> -#include <mbgl/util/run_loop.hpp> -#include <mbgl/util/string.hpp> -#include <mbgl/util/io.hpp> -#include <mbgl/util/logging.hpp> - -using namespace mbgl; - -class StubGlyphManagerObserver : public GlyphManagerObserver { -public: - void onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& glyphRange) override { - if (glyphsLoaded) glyphsLoaded(fontStack, glyphRange); - } - - void onGlyphsError(const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) override { - if (glyphsError) glyphsError(fontStack, glyphRange, error); - } - - std::function<void (const FontStack&, const GlyphRange&)> glyphsLoaded; - std::function<void (const FontStack&, const GlyphRange&, std::exception_ptr)> glyphsError; -}; - -class StubGlyphRequestor : public GlyphRequestor { -public: - void onGlyphsAvailable(GlyphMap glyphs) override { - if (glyphsAvailable) glyphsAvailable(std::move(glyphs)); - } - - std::function<void (GlyphMap)> glyphsAvailable; -}; - -class GlyphManagerTest { -public: - util::RunLoop loop; - StubFileSource fileSource; - StubGlyphManagerObserver observer; - StubGlyphRequestor requestor; - GlyphManager glyphManager { fileSource }; - - void run(const std::string& url, GlyphDependencies dependencies) { - // Squelch logging. - Log::setObserver(std::make_unique<Log::NullObserver>()); - - glyphManager.setURL(url); - glyphManager.setObserver(&observer); - glyphManager.getGlyphs(requestor, std::move(dependencies)); - - loop.run(); - } - - void end() { - loop.stop(); - } -}; - -TEST(GlyphManager, LoadingSuccess) { - GlyphManagerTest test; - - test.fileSource.glyphsResponse = [&] (const Resource& resource) { - EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); - Response response; - response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf")); - return response; - }; - - test.observer.glyphsError = [&] (const FontStack&, const GlyphRange&, std::exception_ptr) { - FAIL(); - test.end(); - }; - - test.observer.glyphsLoaded = [&] (const FontStack& fontStack, const GlyphRange& range) { - ASSERT_EQ(fontStack, FontStack {{"Test Stack"}}); - ASSERT_EQ(range, GlyphRange(0, 255)); - }; - - test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at({{"Test Stack"}}); - - ASSERT_EQ(testPositions.size(), 3u); - ASSERT_EQ(testPositions.count(u'a'), 1u); - ASSERT_EQ(testPositions.count(u'å'), 1u); - ASSERT_EQ(testPositions.count(u' '), 1u); - ASSERT_TRUE(bool(testPositions.at(u' '))); - - test.end(); - }; - - test.run( - "test/fixtures/resources/glyphs.pbf", - GlyphDependencies { - {{{"Test Stack"}}, {u'a', u'å', u' '}} - }); -} - -TEST(GlyphManager, LoadingFail) { - GlyphManagerTest test; - - test.fileSource.glyphsResponse = [&] (const Resource&) { - Response response; - response.error = std::make_unique<Response::Error>( - Response::Error::Reason::Other, - "Failed by the test case"); - return response; - }; - - test.observer.glyphsError = [&] (const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) { - EXPECT_EQ(fontStack, FontStack({"Test Stack"})); - EXPECT_EQ(glyphRange, GlyphRange(0, 255)); - - EXPECT_TRUE(error != nullptr); - EXPECT_EQ(util::toString(error), "Failed by the test case"); - - test.end(); - }; - - test.requestor.glyphsAvailable = [&] (GlyphMap) { - FAIL(); - test.end(); - }; - - test.run( - "test/fixtures/resources/glyphs.pbf", - GlyphDependencies { - {{{"Test Stack"}}, {u'a', u'å'}} - }); -} - -TEST(GlyphManager, LoadingCorrupted) { - GlyphManagerTest test; - - test.fileSource.glyphsResponse = [&] (const Resource&) { - Response response; - response.data = std::make_unique<std::string>("CORRUPTED"); - return response; - }; - - test.observer.glyphsError = [&] (const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) { - EXPECT_EQ(fontStack, FontStack({"Test Stack"})); - EXPECT_EQ(glyphRange, GlyphRange(0, 255)); - - EXPECT_TRUE(error != nullptr); - EXPECT_EQ(util::toString(error), "unknown pbf field type exception"); - - test.end(); - }; - - test.requestor.glyphsAvailable = [&] (GlyphMap) { - FAIL(); - test.end(); - }; - - test.run( - "test/fixtures/resources/glyphs.pbf", - GlyphDependencies { - {{{"Test Stack"}}, {u'a', u'å'}} - }); -} - -TEST(GlyphManager, LoadingCancel) { - GlyphManagerTest test; - - test.fileSource.glyphsResponse = [&] (const Resource&) { - test.end(); - return optional<Response>(); - }; - - test.observer.glyphsLoaded = [&] (const FontStack&, const GlyphRange&) { - FAIL() << "Should never be called"; - }; - - test.run( - "test/fixtures/resources/glyphs.pbf", - GlyphDependencies { - {{{"Test Stack"}}, {u'a', u'å'}} - }); -} - -TEST(GlyphManager, LoadingInvalid) { - GlyphManagerTest test; - - test.fileSource.glyphsResponse = [&] (const Resource& resource) { - EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); - Response response; - response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/fake_glyphs-0-255.pbf")); - return response; - }; - - test.observer.glyphsError = [&] (const FontStack&, const GlyphRange&, std::exception_ptr) { - FAIL(); - test.end(); - }; - - test.observer.glyphsLoaded = [&] (const FontStack& fontStack, const GlyphRange& range) { - ASSERT_EQ(fontStack, FontStack {{"Test Stack"}}); - ASSERT_EQ(range, GlyphRange(0, 255)); - }; - - test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at({{"Test Stack"}}); - - ASSERT_EQ(testPositions.size(), 2u); - ASSERT_FALSE(bool(testPositions.at(u'A'))); - ASSERT_TRUE(bool(testPositions.at(u'E'))); - - test.end(); - }; - - test.run( - "test/fixtures/resources/glyphs.pbf", - GlyphDependencies { - {{{"Test Stack"}}, {u'A', u'E'}} - }); -} - -TEST(GlyphManager, ImmediateFileSource) { - class GlyphManagerTestSynchronous { - public: - util::RunLoop loop; - StubFileSource fileSource = { StubFileSource::ResponseType::Synchronous }; - StubGlyphManagerObserver observer; - StubGlyphRequestor requestor; - GlyphManager glyphManager { fileSource }; - - void run(const std::string& url, GlyphDependencies dependencies) { - // Squelch logging. - Log::setObserver(std::make_unique<Log::NullObserver>()); - - glyphManager.setURL(url); - glyphManager.setObserver(&observer); - glyphManager.getGlyphs(requestor, std::move(dependencies)); - - loop.run(); - } - - void end() { - loop.stop(); - } - }; - - GlyphManagerTestSynchronous test; - - test.fileSource.glyphsResponse = [&] (const Resource&) { - Response response; - response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf")); - return response; - }; - - test.observer.glyphsError = [&] (const FontStack&, const GlyphRange&, std::exception_ptr) { - FAIL(); - test.end(); - }; - - test.requestor.glyphsAvailable = [&] (GlyphMap) { - test.end(); - }; - - test.run( - "test/fixtures/resources/glyphs.pbf", - GlyphDependencies { - {{{"Test Stack"}}, {u'a', u'å', u' '}} - }); -} diff --git a/test/text/glyph_manager.test.cpp b/test/text/glyph_manager.test.cpp new file mode 100644 index 0000000000..a96e1b970c --- /dev/null +++ b/test/text/glyph_manager.test.cpp @@ -0,0 +1,341 @@ +#include <mbgl/test/util.hpp> +#include <mbgl/test/stub_file_source.hpp> + +#include <mbgl/text/glyph_manager.hpp> +#include <mbgl/util/run_loop.hpp> +#include <mbgl/util/string.hpp> +#include <mbgl/util/i18n.hpp> +#include <mbgl/util/io.hpp> +#include <mbgl/util/logging.hpp> + +using namespace mbgl; + +// Alpha channel rendering of '中' +static constexpr const uint8_t stubBitmap[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 95, 82, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 255, 227, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 22, 18, 18, 18, 18, 18, 15, 55, 255, 255, 42, 14, 18, 18, 18, 18, 18, 22, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 167, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 149, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 178, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 255, 120, 8, 15, 15, 15, 11, 51, 255, 233, 32, 11, 15, 15, 15, 8, 93, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 255, 100, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 84, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 255, 107, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 84, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 255, 107, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 84, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 255, 100, 0, 0, 0, 0, 0, 32, 255, 227, 10, 0, 0, 0, 0, 0, 78, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 255, 170, 101, 101, 101, 101, 101, 129, 255, 255, 133, 100, 101, 101, 101, 96, 191, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 105, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 178, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 156, 255, 172, 110, 116, 116, 116, 115, 140, 255, 242, 127, 116, 116, 116, 116, 110, 163, 255, 169, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 255, 108, 0, 0, 0, 0, 0, 32, 255, 227, 10, 0, 0, 0, 0, 0, 89, 255, 194, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 27, 16, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 14, 27, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 233, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 205, 167, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +// SDF transformation of '中' +static constexpr const uint8_t sdfBitmap[] = {0, 0, 0, 0, 0, 0, 0, 0, 19, 48, 76, 101, 119, 127, 127, 127, 126, 118, 100, 75, 48, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 19, 28, 31, 31, 31, 31, 59, 90, 119, 145, 158, 159, 159, 156, 144, 118, 89, 59, 31, 31, 31, 31, 28, 19, 5, 0, 0, 0, 10, 31, 48, 59, 63, 63, 63, 63, 63, 95, 127, 158, 182, 187, 186, 176, 156, 126, 94, 63, 63, 63, 63, 63, 59, 48, 31, 10, 0, 5, 31, 55, 75, 89, 94, 95, 95, 95, 95, 95, 127, 158, 181, 223, 204, 177, 157, 126, 95, 95, 95, 95, 95, 95, 89, 75, 55, 31, 10, 19, 48, 75, 100, 118, 126, 126, 126, 126, 126, 126, 127, 157, 180, 223, 204, 177, 157, 126, 126, 126, 126, 126, 126, 126, 119, 100, 75, 55, 31, 28, 59, 89, 118, 144, 156, 157, 157, 157, 157, 157, 157, 157, 180, 223, 204, 177, 156, 157, 157, 157, 157, 157, 157, 156, 144, 119, 100, 75, 48, 31, 63, 94, 126, 156, 176, 178, 178, 178, 178, 178, 178, 177, 182, 223, 223, 181, 177, 178, 178, 178, 178, 178, 178, 177, 156, 144, 118, 89, 59, 32, 64, 96, 128, 159, 193, 223, 223, 223, 223, 223, 223, 223, 223, 236, 236, 223, 223, 223, 223, 223, 223, 223, 223, 196, 176, 156, 126, 94, 63, 32, 64, 96, 128, 159, 194, 223, 223, 223, 223, 223, 223, 223, 223, 236, 226, 223, 223, 223, 223, 223, 223, 223, 224, 198, 176, 156, 126, 94, 63, 32, 64, 96, 127, 159, 187, 223, 190, 176, 177, 177, 177, 177, 182, 223, 204, 179, 177, 177, 177, 177, 176, 187, 223, 198, 176, 156, 126, 94, 63, 32, 64, 95, 127, 159, 186, 223, 188, 159, 156, 156, 156, 157, 180, 223, 204, 177, 157, 156, 156, 156, 159, 186, 223, 198, 176, 156, 126, 94, 63, 32, 64, 95, 127, 159, 186, 223, 189, 159, 127, 126, 127, 157, 180, 223, 204, 177, 157, 126, 126, 127, 159, 186, 223, 198, 176, 156, 126, 94, 63, 32, 64, 95, 127, 159, 186, 223, 189, 159, 127, 127, 127, 157, 180, 223, 204, 177, 157, 127, 127, 127, 159, 186, 223, 198, 176, 156, 126, 94, 63, 32, 64, 95, 127, 159, 186, 223, 188, 159, 159, 159, 159, 159, 179, 223, 204, 177, 159, 159, 159, 159, 159, 185, 223, 198, 176, 156, 126, 94, 63, 48, 75, 100, 127, 159, 186, 223, 197, 188, 188, 188, 188, 188, 191, 223, 223, 192, 188, 188, 188, 188, 187, 199, 224, 198, 176, 156, 126, 94, 63, 59, 89, 118, 143, 159, 188, 223, 224, 223, 223, 223, 223, 223, 223, 236, 226, 223, 223, 223, 223, 223, 223, 223, 224, 198, 176, 156, 126, 94, 63, 63, 94, 126, 156, 175, 195, 223, 197, 189, 190, 190, 190, 190, 193, 223, 206, 191, 190, 190, 190, 190, 189, 196, 223, 196, 176, 156, 126, 94, 63, 59, 89, 118, 143, 159, 193, 223, 189, 159, 159, 159, 159, 159, 179, 223, 204, 177, 159, 159, 159, 159, 159, 186, 223, 200, 176, 156, 126, 94, 63, 48, 75, 100, 126, 156, 176, 179, 177, 156, 127, 127, 127, 157, 180, 223, 204, 177, 157, 127, 127, 127, 156, 177, 179, 178, 157, 144, 118, 89, 59, 31, 59, 89, 118, 144, 156, 157, 156, 144, 119, 96, 127, 157, 180, 223, 204, 177, 157, 126, 96, 119, 144, 156, 157, 157, 144, 119, 100, 75, 48, 19, 48, 75, 100, 118, 126, 126, 126, 119, 100, 95, 127, 157, 180, 223, 204, 177, 157, 126, 95, 100, 119, 126, 126, 126, 119, 100, 76, 55, 31, 5, 31, 55, 75, 89, 94, 95, 95, 89, 75, 95, 127, 157, 180, 223, 204, 177, 157, 126, 95, 75, 89, 95, 95, 95, 90, 76, 55, 31, 10, 0, 10, 31, 48, 59, 63, 63, 63, 59, 63, 95, 127, 157, 180, 223, 204, 177, 157, 126, 95, 63, 59, 63, 63, 63, 59, 48, 31, 10, 0, 0, 0, 5, 19, 28, 31, 31, 31, 31, 63, 95, 127, 157, 180, 223, 204, 177, 157, 126, 95, 63, 31, 31, 31, 31, 28, 19, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 64, 95, 127, 159, 184, 201, 196, 176, 156, 126, 94, 63, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 60, 90, 120, 146, 159, 159, 159, 156, 144, 118, 89, 59, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 49, 76, 101, 120, 127, 128, 128, 126, 118, 100, 75, 48, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 32, 56, 76, 90, 95, 96, 96, 94, 89, 75, 55, 31, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 32, 49, 60, 64, 64, 64, 63, 59, 48, 31, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 19, 29, 32, 32, 32, 31, 28, 19, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +static constexpr const size_t stubBitmapLength = 900; + +class StubLocalGlyphRasterizer : public LocalGlyphRasterizer { +public: + bool canRasterizeGlyph(const FontStack&, GlyphID glyphID) { + return util::i18n::allowsIdeographicBreaking(glyphID); + } + + Glyph rasterizeGlyph(const FontStack&, GlyphID glyphID) { + Glyph stub; + stub.id = glyphID; + + stub.metrics.width = 24; + stub.metrics.height = 24; + stub.metrics.left = 0; + stub.metrics.top = -8; + stub.metrics.advance = 24; + + stub.bitmap = AlphaImage(Size(30, 30), stubBitmap, stubBitmapLength); + + return stub; + } +}; + +class StubGlyphManagerObserver : public GlyphManagerObserver { +public: + void onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& glyphRange) override { + if (glyphsLoaded) glyphsLoaded(fontStack, glyphRange); + } + + void onGlyphsError(const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) override { + if (glyphsError) glyphsError(fontStack, glyphRange, error); + } + + std::function<void (const FontStack&, const GlyphRange&)> glyphsLoaded; + std::function<void (const FontStack&, const GlyphRange&, std::exception_ptr)> glyphsError; +}; + +class StubGlyphRequestor : public GlyphRequestor { +public: + void onGlyphsAvailable(GlyphMap glyphs) override { + if (glyphsAvailable) glyphsAvailable(std::move(glyphs)); + } + + std::function<void (GlyphMap)> glyphsAvailable; +}; + +class GlyphManagerTest { +public: + util::RunLoop loop; + StubFileSource fileSource; + StubGlyphManagerObserver observer; + StubGlyphRequestor requestor; + GlyphManager glyphManager{ fileSource, std::make_unique<StubLocalGlyphRasterizer>() }; + + void run(const std::string& url, GlyphDependencies dependencies) { + // Squelch logging. + Log::setObserver(std::make_unique<Log::NullObserver>()); + + glyphManager.setURL(url); + glyphManager.setObserver(&observer); + glyphManager.getGlyphs(requestor, std::move(dependencies)); + + loop.run(); + } + + void end() { + loop.stop(); + } +}; + +TEST(GlyphManager, LoadingSuccess) { + GlyphManagerTest test; + + test.fileSource.glyphsResponse = [&] (const Resource& resource) { + EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); + Response response; + response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf")); + return response; + }; + + test.observer.glyphsError = [&] (const FontStack&, const GlyphRange&, std::exception_ptr) { + FAIL(); + test.end(); + }; + + test.observer.glyphsLoaded = [&] (const FontStack& fontStack, const GlyphRange& range) { + ASSERT_EQ(fontStack, FontStack {{"Test Stack"}}); + ASSERT_EQ(range, GlyphRange(0, 255)); + }; + + test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { + const auto& testPositions = glyphs.at({{"Test Stack"}}); + + ASSERT_EQ(testPositions.size(), 3u); + ASSERT_EQ(testPositions.count(u'a'), 1u); + ASSERT_EQ(testPositions.count(u'å'), 1u); + ASSERT_EQ(testPositions.count(u' '), 1u); + ASSERT_TRUE(bool(testPositions.at(u' '))); + + test.end(); + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'a', u'å', u' '}} + }); +} + +TEST(GlyphManager, LoadingFail) { + GlyphManagerTest test; + + test.fileSource.glyphsResponse = [&] (const Resource&) { + Response response; + response.error = std::make_unique<Response::Error>( + Response::Error::Reason::Other, + "Failed by the test case"); + return response; + }; + + test.observer.glyphsError = [&] (const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) { + EXPECT_EQ(fontStack, FontStack({"Test Stack"})); + EXPECT_EQ(glyphRange, GlyphRange(0, 255)); + + EXPECT_TRUE(error != nullptr); + EXPECT_EQ(util::toString(error), "Failed by the test case"); + + test.end(); + }; + + test.requestor.glyphsAvailable = [&] (GlyphMap) { + FAIL(); + test.end(); + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'a', u'å'}} + }); +} + +TEST(GlyphManager, LoadingCorrupted) { + GlyphManagerTest test; + + test.fileSource.glyphsResponse = [&] (const Resource&) { + Response response; + response.data = std::make_unique<std::string>("CORRUPTED"); + return response; + }; + + test.observer.glyphsError = [&] (const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) { + EXPECT_EQ(fontStack, FontStack({"Test Stack"})); + EXPECT_EQ(glyphRange, GlyphRange(0, 255)); + + EXPECT_TRUE(error != nullptr); + EXPECT_EQ(util::toString(error), "unknown pbf field type exception"); + + test.end(); + }; + + test.requestor.glyphsAvailable = [&] (GlyphMap) { + FAIL(); + test.end(); + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'a', u'å'}} + }); +} + +TEST(GlyphManager, LoadingCancel) { + GlyphManagerTest test; + + test.fileSource.glyphsResponse = [&] (const Resource&) { + test.end(); + return optional<Response>(); + }; + + test.observer.glyphsLoaded = [&] (const FontStack&, const GlyphRange&) { + FAIL() << "Should never be called"; + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'a', u'å'}} + }); +} + +TEST(GlyphManager, LoadLocalCJKGlyph) { + GlyphManagerTest test; + int glyphResponses = 0; + + test.fileSource.glyphsResponse = [&] (const Resource&) { + glyphResponses++; + return optional<Response>(); + }; + + test.observer.glyphsLoaded = [&] (const FontStack&, const GlyphRange&) { + glyphResponses++; + }; + + test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { + EXPECT_EQ(glyphResponses, 0); // Local generation should prevent requesting any glyphs + + const auto& testPositions = glyphs.at({{"Test Stack"}}); + + ASSERT_EQ(testPositions.size(), 1u); + ASSERT_EQ(testPositions.count(u'中'), 1u); + + Immutable<Glyph> glyph = *testPositions.at(u'中'); + EXPECT_EQ(glyph->id, u'中'); + EXPECT_EQ(glyph->metrics.width, 24ul); + EXPECT_EQ(glyph->metrics.height, 24ul); + EXPECT_EQ(glyph->metrics.left, 0); + EXPECT_EQ(glyph->metrics.top, -8); + EXPECT_EQ(glyph->metrics.advance, 24ul); + EXPECT_EQ(glyph->bitmap.size, Size(30, 30)); + + size_t pixelCount = glyph->bitmap.size.width * glyph->bitmap.size.height; + for (size_t i = 0; i < pixelCount; i++) { + EXPECT_EQ(glyph->bitmap.data[i], sdfBitmap[i]); + } + + test.end(); + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'中'}} + }); +} + + +TEST(GlyphManager, LoadingInvalid) { + GlyphManagerTest test; + + test.fileSource.glyphsResponse = [&] (const Resource& resource) { + EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); + Response response; + response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/fake_glyphs-0-255.pbf")); + return response; + }; + + test.observer.glyphsError = [&] (const FontStack&, const GlyphRange&, std::exception_ptr) { + FAIL(); + test.end(); + }; + + test.observer.glyphsLoaded = [&] (const FontStack& fontStack, const GlyphRange& range) { + ASSERT_EQ(fontStack, FontStack {{"Test Stack"}}); + ASSERT_EQ(range, GlyphRange(0, 255)); + }; + + test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { + const auto& testPositions = glyphs.at({{"Test Stack"}}); + + ASSERT_EQ(testPositions.size(), 2u); + ASSERT_FALSE(bool(testPositions.at(u'A'))); + ASSERT_TRUE(bool(testPositions.at(u'E'))); + + test.end(); + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'A', u'E'}} + }); +} + +TEST(GlyphManager, ImmediateFileSource) { + class GlyphManagerTestSynchronous { + public: + util::RunLoop loop; + StubFileSource fileSource = { StubFileSource::ResponseType::Synchronous }; + StubGlyphManagerObserver observer; + StubGlyphRequestor requestor; + GlyphManager glyphManager { fileSource }; + + void run(const std::string& url, GlyphDependencies dependencies) { + // Squelch logging. + Log::setObserver(std::make_unique<Log::NullObserver>()); + + glyphManager.setURL(url); + glyphManager.setObserver(&observer); + glyphManager.getGlyphs(requestor, std::move(dependencies)); + + loop.run(); + } + + void end() { + loop.stop(); + } + }; + + GlyphManagerTestSynchronous test; + + test.fileSource.glyphsResponse = [&] (const Resource&) { + Response response; + response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf")); + return response; + }; + + test.observer.glyphsError = [&] (const FontStack&, const GlyphRange&, std::exception_ptr) { + FAIL(); + test.end(); + }; + + test.requestor.glyphsAvailable = [&] (GlyphMap) { + test.end(); + }; + + test.run( + "test/fixtures/resources/glyphs.pbf", + GlyphDependencies { + {{{"Test Stack"}}, {u'a', u'å', u' '}} + }); +} |