From 3a2f13d4cfc96ad9a13c18280f158e26ca89de7f Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Wed, 21 Aug 2019 09:44:15 -0700 Subject: [PATCH 01/33] port to protozero from @joto - refs #143 --- binding.gyp | 31 ++++------------------------- scripts/install_deps.sh | 8 ++------ scripts/setup.sh | 2 +- src/glyphs.cpp | 43 ++++++++++++++++++++--------------------- src/glyphs.hpp | 1 - 5 files changed, 28 insertions(+), 57 deletions(-) diff --git a/binding.gyp b/binding.gyp index 3acdaa0fe..a37102eb6 100644 --- a/binding.gyp +++ b/binding.gyp @@ -9,8 +9,7 @@ 'system_includes': [ "-isystem <(module_root_dir)/ // node #include #include @@ -291,7 +291,7 @@ void RangeAsync(uv_work_t* req) { /* LCOV_EXCL_END */ } - llmr::glyphs::glyphs glyphs; + protozero::pbf_writer pbf_writer{baton->message}; FT_Face ft_face = 0; FT_Long num_faces = 0; for (int i = 0; ft_face == 0 || i < num_faces; ++i) { @@ -307,14 +307,13 @@ void RangeAsync(uv_work_t* req) { } if (ft_face->family_name) { - llmr::glyphs::fontstack* mutable_fontstack = glyphs.add_stacks(); + protozero::pbf_writer fontstack_writer{pbf_writer, 1}; if (ft_face->style_name) { - mutable_fontstack->set_name(std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); + fontstack_writer.add_string(1,std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); } else { - mutable_fontstack->set_name(std::string(ft_face->family_name)); + fontstack_writer.add_string(1,std::string(ft_face->family_name)); } - - mutable_fontstack->set_range(std::to_string(baton->start) + "-" + std::to_string(baton->end)); + fontstack_writer.add_string(2,std::to_string(baton->start) + "-" + std::to_string(baton->end)); const double scale_factor = 1.0; @@ -335,47 +334,47 @@ void RangeAsync(uv_work_t* req) { sdf_glyph_foundry::RenderSDF(glyph, 24, 3, 0.25, ft_face); // Add glyph to fontstack. - llmr::glyphs::glyph* mutable_glyph = mutable_fontstack->add_glyphs(); - - // direct type conversions, no need for checking or casting - mutable_glyph->set_width(glyph.width); - mutable_glyph->set_height(glyph.height); - mutable_glyph->set_left(glyph.left); - - // conversions requiring checks, for safety and correctness + protozero::pbf_writer glyph_writer{fontstack_writer, 3}; // shortening conversion if (char_code > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for char_code: too large"); } else { - mutable_glyph->set_id(static_cast(char_code)); + glyph_writer.add_uint32(1,static_cast(char_code)); } + if (glyph.width > 0) { + glyph_writer.add_bytes(2,glyph.bitmap); + } + + // direct type conversions, no need for checking or casting + glyph_writer.add_uint32(3,glyph.width); + glyph_writer.add_uint32(4,glyph.width); + glyph_writer.add_sint32(5,glyph.width); + + // conversions requiring checks, for safety and correctness + // double to int double top = static_cast(glyph.top) - glyph.ascender; if (top < std::numeric_limits::min() || top > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); } else { - mutable_glyph->set_top(static_cast(top)); + glyph_writer.add_sint32(6,static_cast(top)); } // double to uint if (glyph.advance < std::numeric_limits::min() || glyph.advance > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); } else { - mutable_glyph->set_advance(static_cast(glyph.advance)); + glyph_writer.add_uint32(7,static_cast(glyph.advance)); } - if (glyph.width > 0) { - mutable_glyph->set_bitmap(glyph.bitmap); - } } } else { baton->error_name = std::string("font does not have family_name"); return; } } - baton->message = glyphs.SerializeAsString(); } catch (std::exception const& ex) { baton->error_name = ex.what(); } diff --git a/src/glyphs.hpp b/src/glyphs.hpp index ecfc8cb94..173872cf0 100644 --- a/src/glyphs.hpp +++ b/src/glyphs.hpp @@ -1,7 +1,6 @@ #ifndef NODE_FONTNIK_GLYPHS_HPP #define NODE_FONTNIK_GLYPHS_HPP -#include "glyphs.pb.h" #include namespace node_fontnik { From 06d0cb4529ec62d95520a6f0a25d24c15ba00d41 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Sat, 24 Aug 2019 14:30:38 -0700 Subject: [PATCH 02/33] try to work around travis node_modules caching --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a2f661376..065f488cc 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,16 @@ export WERROR ?= false default: release -node_modules: +./node_modules/.bin/node-pre-gyp: # install deps but for now ignore our own install script # so that we can run it directly in either debug or release npm install --ignore-scripts -release: node_modules +release: ./node_modules/.bin/node-pre-gyp V=1 ./node_modules/.bin/node-pre-gyp configure build --error_on_warnings=$(WERROR) --loglevel=error @echo "run 'make clean' for full rebuild" -debug: node_modules +debug: ./node_modules/.bin/node-pre-gyp V=1 ./node_modules/.bin/node-pre-gyp configure build --error_on_warnings=$(WERROR) --loglevel=error --debug @echo "run 'make clean' for full rebuild" @@ -46,7 +46,7 @@ distclean: clean rm -rf .toolchain rm -f local.env -xcode: node_modules +xcode: ./node_modules/.bin/node-pre-gyp ./node_modules/.bin/node-pre-gyp configure -- -f xcode @# If you need more targets, e.g. to run other npm scripts, duplicate the last line and change NPM_ARGUMENT From 209129c2a9d98bf02758f3fcb711ebb5708fb098 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 26 Aug 2019 08:18:39 -0700 Subject: [PATCH 03/33] implement glyph compositing (port from https://github.com/mapbox/glyph-pbf-composite) --- binding.gyp | 5 +- common.gypi | 2 +- scripts/install_deps.sh | 3 +- src/glyphs.cpp | 239 ++++++++++++++++++++++++++++++++++++++++ src/glyphs.hpp | 3 + src/node_fontnik.cpp | 1 + 6 files changed, 249 insertions(+), 4 deletions(-) diff --git a/binding.gyp b/binding.gyp index a37102eb6..364620afa 100644 --- a/binding.gyp +++ b/binding.gyp @@ -25,7 +25,8 @@ '-Wuninitialized', '-Wunreachable-code', '-Wold-style-cast', - '-Wno-error=unused-variable' + '-Wno-error=unused-variable', + '-Wno-deprecated-declarations' ] }, # `targets` is a list of targets for gyp to run. @@ -105,7 +106,7 @@ 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', 'MACOSX_DEPLOYMENT_TARGET':'10.8', 'CLANG_CXX_LIBRARY': 'libc++', - 'CLANG_CXX_LANGUAGE_STANDARD':'c++11', + 'CLANG_CXX_LANGUAGE_STANDARD':'c++14', 'GCC_VERSION': 'com.apple.compilers.llvm.clang.1_0' } } diff --git a/common.gypi b/common.gypi index 53d76b35f..824017267 100644 --- a/common.gypi +++ b/common.gypi @@ -2,7 +2,7 @@ 'target_defaults': { 'default_configuration': 'Release', 'cflags_cc' : [ - '-std=c++11', + '-std=c++14', # The assumption is that projects based on node-cpp-skel will also # depend on mason packages. Currently (this will change in future mason versions) # mason packages default to being built/linked with the CXX11_ABI=0. diff --git a/scripts/install_deps.sh b/scripts/install_deps.sh index 6ac457bb4..e11ea7741 100755 --- a/scripts/install_deps.sh +++ b/scripts/install_deps.sh @@ -15,4 +15,5 @@ source local.env install boost 1.63.0 install freetype 2.7.1 install protozero 1.6.8 -install sdf-glyph-foundry 0.1.1 \ No newline at end of file +install sdf-glyph-foundry 0.1.1 +install gzip-hpp 0.1.0 \ No newline at end of file diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 8f620244f..a08f7552d 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -1,14 +1,19 @@ // fontnik #include "glyphs.hpp" +#include #include // node #include #include #include +#include // sdf-glyph-foundry #include #include +#include +#include +#include namespace node_fontnik { @@ -101,6 +106,54 @@ struct RangeBaton { } }; +struct GlyphPBF +{ + GlyphPBF(v8::Local& buffer) + : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)}, + buffer_ref{} + { + buffer_ref.Reset(buffer.As()); + } + + ~GlyphPBF() + { + buffer_ref.Reset(); + } + + // non-copyable + GlyphPBF(GlyphPBF const&) = delete; + GlyphPBF& operator=(GlyphPBF const&) = delete; + + // non-movable + GlyphPBF(GlyphPBF&&) = delete; + GlyphPBF& operator=(GlyphPBF&&) = delete; + + protozero::data_view data; + Nan::Persistent buffer_ref; +}; + +struct CompositeBaton { + CompositeBaton(CompositeBaton const&) = delete; + CompositeBaton& operator=(CompositeBaton const&) = delete; + + Nan::Persistent callback; + std::vector> glyphs{}; + std::string error_name; + std::unique_ptr message; + uv_work_t request; + CompositeBaton(unsigned size, v8::Local cb) : + error_name(), + message(std::make_unique()), + request() { + glyphs.reserve(size); + request.data = this; + callback.Reset(cb.As()); + } + ~CompositeBaton() { + callback.Reset(); + } +}; + NAN_METHOD(Load) { // Validate arguments. if (!info[0]->IsObject()) { @@ -161,6 +214,192 @@ NAN_METHOD(Range) { uv_queue_work(uv_default_loop(), &baton->request, RangeAsync, reinterpret_cast(AfterRange)); } +namespace utils { + + inline void CallbackError(std::string message, v8::Local func) + { + Nan::Callback cb(func); + v8::Local argv[1] = {Nan::Error(message.c_str())}; + Nan::Call(cb, 1, argv); + } + +} + +NAN_METHOD(Composite) { + // validate callback function + v8::Local callback_val = info[info.Length() - 1]; + if (!callback_val->IsFunction()) + { + Nan::ThrowError("last argument must be a callback function"); + return; + } + + v8::Local callback = callback_val.As(); + + // validate glyphPBF array + if (!info[0]->IsArray()) + { + return utils::CallbackError("first arg 'glyphs' must be an array of glyphs objects", callback); + } + + v8::Local glyphs = info[0].As(); + unsigned num_glyphs = glyphs->Length(); + + if (num_glyphs <= 0) + { + return utils::CallbackError("'glyphs' array must be of length greater than 0", callback); + } + + CompositeBaton* baton = new CompositeBaton(num_glyphs,callback); + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + + for (unsigned t = 0; t < num_glyphs; ++t) + { + v8::Local buf_val = glyphs->Get(t); + if (buf_val->IsNull() || buf_val->IsUndefined()) + { + return utils::CallbackError("buffer value in 'glyphs' array item is null or undefined", callback); + } + v8::MaybeLocal maybe_buffer = buf_val->ToObject(isolate->GetCurrentContext()); + if (maybe_buffer.IsEmpty()) + { + return utils::CallbackError("buffer value in 'glyphs' array is empty", callback); + } + v8::Local buffer = maybe_buffer.ToLocalChecked(); + + if (!node::Buffer::HasInstance(buffer)) + { + return utils::CallbackError("buffer value in 'glyphs' array item is not a true buffer", callback); + } + baton->glyphs.push_back(std::make_unique(buffer)); + } + uv_queue_work(uv_default_loop(), &baton->request, CompositeAsync, reinterpret_cast(AfterComposite)); +} + +using id_pair = std::pair; +struct CompareID { + bool operator()(id_pair const& r1, id_pair const& r2) { + return r1.first - r2.first; + } +}; + +void CompositeAsync(uv_work_t* req) { + CompositeBaton* baton = static_cast(req->data); + try { + std::vector>> buffer_cache; + std::map id_mapping; + bool first_buffer = true; + std::string fontstack_name; + std::string range; + std::string& fontstack_buffer = *baton->message.get(); + protozero::pbf_writer pbf_writer(fontstack_buffer); + protozero::pbf_writer fontstack_writer{pbf_writer, 1}; + // TODO(danespringmeyer): avoid duplicate fontstacks to be sent it + for (auto const& glyph_obj : baton->glyphs) { + protozero::data_view data_view{}; + if (gzip::is_compressed(glyph_obj->data.data(), glyph_obj->data.size())) + { + buffer_cache.push_back(std::make_unique>()); + gzip::Decompressor decompressor; + decompressor.decompress(*buffer_cache.back(), glyph_obj->data.data(), glyph_obj->data.size()); + data_view = protozero::data_view{buffer_cache.back()->data(), buffer_cache.back()->size()}; + } + else + { + data_view = glyph_obj->data; + } + protozero::pbf_reader fontstack_reader(data_view); + while (fontstack_reader.next(1)) { + auto stack_reader = fontstack_reader.get_message(); + while (stack_reader.next()) { + switch (stack_reader.tag()) { + case 1: // name + { + if (first_buffer) { + fontstack_name = stack_reader.get_string(); + } else { + fontstack_name = fontstack_name + ", " + stack_reader.get_string(); + } + break; + } + case 2: // range + { + if (first_buffer) { + range = stack_reader.get_string(); + } else { + stack_reader.skip(); + } + break; + } + case 3: // glyphs + { + auto glyphs_data = stack_reader.get_view(); + // collect all ids from first + if (first_buffer) { + protozero::pbf_reader glyphs_reader(glyphs_data); + std::uint32_t glyph_id; + while (glyphs_reader.next(1)) { + glyph_id = glyphs_reader.get_uint32(); + } + id_mapping.emplace(glyph_id,glyphs_data); + } else { + protozero::pbf_reader glyphs_reader(glyphs_data); + std::uint32_t glyph_id; + while (glyphs_reader.next(1)) { + glyph_id = glyphs_reader.get_uint32(); + } + auto search = id_mapping.find(glyph_id); + if (search == id_mapping.end()) { + id_mapping.emplace(glyph_id,glyphs_data); + } + } + break; + } + default: + // ignore data for unknown tags to allow for future extensions + stack_reader.skip(); + } + } + } + first_buffer = false; + } + fontstack_writer.add_string(1,fontstack_name); + fontstack_writer.add_string(2,range); + for (auto const& glyph_pair : id_mapping) { + fontstack_writer.add_message(3,glyph_pair.second); + } + } catch (std::exception const& ex) { + baton->error_name = ex.what(); + } +} + +void AfterComposite(uv_work_t* req) { + Nan::HandleScope scope; + + CompositeBaton* baton = static_cast(req->data); + + if (!baton->error_name.empty()) { + v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; + Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); + } else { + std::string& fontstack_message = *baton->message.get(); + const auto argc = 2u; + v8::Local argv[argc] = { + Nan::Null(), + Nan::NewBuffer(&fontstack_message[0], + static_cast(fontstack_message.size()), + [](char*, void* hint) { + delete reinterpret_cast(hint); + }, + baton->message.release()) + .ToLocalChecked()}; + Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); + } + + delete baton; +} + struct ft_library_guard { // non copyable ft_library_guard(ft_library_guard const&) = delete; diff --git a/src/glyphs.hpp b/src/glyphs.hpp index 173872cf0..ced817a83 100644 --- a/src/glyphs.hpp +++ b/src/glyphs.hpp @@ -11,6 +11,9 @@ void AfterLoad(uv_work_t* req); NAN_METHOD(Range); void RangeAsync(uv_work_t* req); void AfterRange(uv_work_t* req); +NAN_METHOD(Composite); +void CompositeAsync(uv_work_t* req); +void AfterComposite(uv_work_t* req); } // namespace node_fontnik diff --git a/src/node_fontnik.cpp b/src/node_fontnik.cpp index e90b8e1f6..22753e658 100644 --- a/src/node_fontnik.cpp +++ b/src/node_fontnik.cpp @@ -7,6 +7,7 @@ namespace node_fontnik { NAN_MODULE_INIT(RegisterModule) { target->Set(Nan::New("load").ToLocalChecked(), Nan::New(Load)->GetFunction()); target->Set(Nan::New("range").ToLocalChecked(), Nan::New(Range)->GetFunction()); + target->Set(Nan::New("composite").ToLocalChecked(), Nan::New(Composite)->GetFunction()); } // We mark this NOLINT to avoid the clang-tidy checks From f2c5462ca3e8d5bcbbf6c521c1538ed40b511648 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 26 Aug 2019 11:00:45 -0700 Subject: [PATCH 04/33] make format --- src/glyphs.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 8f620244f..9746c0908 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -309,11 +309,11 @@ void RangeAsync(uv_work_t* req) { if (ft_face->family_name) { protozero::pbf_writer fontstack_writer{pbf_writer, 1}; if (ft_face->style_name) { - fontstack_writer.add_string(1,std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); + fontstack_writer.add_string(1, std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); } else { - fontstack_writer.add_string(1,std::string(ft_face->family_name)); + fontstack_writer.add_string(1, std::string(ft_face->family_name)); } - fontstack_writer.add_string(2,std::to_string(baton->start) + "-" + std::to_string(baton->end)); + fontstack_writer.add_string(2, std::to_string(baton->start) + "-" + std::to_string(baton->end)); const double scale_factor = 1.0; @@ -340,17 +340,17 @@ void RangeAsync(uv_work_t* req) { if (char_code > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for char_code: too large"); } else { - glyph_writer.add_uint32(1,static_cast(char_code)); + glyph_writer.add_uint32(1, static_cast(char_code)); } if (glyph.width > 0) { - glyph_writer.add_bytes(2,glyph.bitmap); + glyph_writer.add_bytes(2, glyph.bitmap); } // direct type conversions, no need for checking or casting - glyph_writer.add_uint32(3,glyph.width); - glyph_writer.add_uint32(4,glyph.width); - glyph_writer.add_sint32(5,glyph.width); + glyph_writer.add_uint32(3, glyph.width); + glyph_writer.add_uint32(4, glyph.width); + glyph_writer.add_sint32(5, glyph.width); // conversions requiring checks, for safety and correctness @@ -359,16 +359,15 @@ void RangeAsync(uv_work_t* req) { if (top < std::numeric_limits::min() || top > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); } else { - glyph_writer.add_sint32(6,static_cast(top)); + glyph_writer.add_sint32(6, static_cast(top)); } // double to uint if (glyph.advance < std::numeric_limits::min() || glyph.advance > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); } else { - glyph_writer.add_uint32(7,static_cast(glyph.advance)); + glyph_writer.add_uint32(7, static_cast(glyph.advance)); } - } } else { baton->error_name = std::string("font does not have family_name"); From ade6735f5c06275047be71caed231d1a2fbcbca9 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 26 Aug 2019 11:15:34 -0700 Subject: [PATCH 05/33] make format --- src/glyphs.cpp | 189 +++++++++++++++++++++++-------------------------- 1 file changed, 87 insertions(+), 102 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 4dc543a90..1f3583dfc 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -4,16 +4,16 @@ #include // node #include +#include #include #include -#include // sdf-glyph-foundry -#include -#include #include #include #include +#include +#include namespace node_fontnik { @@ -106,17 +106,14 @@ struct RangeBaton { } }; -struct GlyphPBF -{ +struct GlyphPBF { GlyphPBF(v8::Local& buffer) : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)}, - buffer_ref{} - { + buffer_ref{} { buffer_ref.Reset(buffer.As()); } - ~GlyphPBF() - { + ~GlyphPBF() { buffer_ref.Reset(); } @@ -141,10 +138,9 @@ struct CompositeBaton { std::string error_name; std::unique_ptr message; uv_work_t request; - CompositeBaton(unsigned size, v8::Local cb) : - error_name(), - message(std::make_unique()), - request() { + CompositeBaton(unsigned size, v8::Local cb) : error_name(), + message(std::make_unique()), + request() { glyphs.reserve(size); request.data = this; callback.Reset(cb.As()); @@ -216,60 +212,52 @@ NAN_METHOD(Range) { namespace utils { - inline void CallbackError(std::string message, v8::Local func) - { - Nan::Callback cb(func); - v8::Local argv[1] = {Nan::Error(message.c_str())}; - Nan::Call(cb, 1, argv); - } - +inline void CallbackError(std::string message, v8::Local func) { + Nan::Callback cb(func); + v8::Local argv[1] = {Nan::Error(message.c_str())}; + Nan::Call(cb, 1, argv); } +} // namespace utils + NAN_METHOD(Composite) { // validate callback function v8::Local callback_val = info[info.Length() - 1]; - if (!callback_val->IsFunction()) - { + if (!callback_val->IsFunction()) { Nan::ThrowError("last argument must be a callback function"); return; } - + v8::Local callback = callback_val.As(); - + // validate glyphPBF array - if (!info[0]->IsArray()) - { + if (!info[0]->IsArray()) { return utils::CallbackError("first arg 'glyphs' must be an array of glyphs objects", callback); } - + v8::Local glyphs = info[0].As(); unsigned num_glyphs = glyphs->Length(); - - if (num_glyphs <= 0) - { + + if (num_glyphs <= 0) { return utils::CallbackError("'glyphs' array must be of length greater than 0", callback); } - - CompositeBaton* baton = new CompositeBaton(num_glyphs,callback); - + + CompositeBaton* baton = new CompositeBaton(num_glyphs, callback); + v8::Isolate* isolate = v8::Isolate::GetCurrent(); - for (unsigned t = 0; t < num_glyphs; ++t) - { + for (unsigned t = 0; t < num_glyphs; ++t) { v8::Local buf_val = glyphs->Get(t); - if (buf_val->IsNull() || buf_val->IsUndefined()) - { + if (buf_val->IsNull() || buf_val->IsUndefined()) { return utils::CallbackError("buffer value in 'glyphs' array item is null or undefined", callback); } v8::MaybeLocal maybe_buffer = buf_val->ToObject(isolate->GetCurrentContext()); - if (maybe_buffer.IsEmpty()) - { + if (maybe_buffer.IsEmpty()) { return utils::CallbackError("buffer value in 'glyphs' array is empty", callback); } v8::Local buffer = maybe_buffer.ToLocalChecked(); - if (!node::Buffer::HasInstance(buffer)) - { + if (!node::Buffer::HasInstance(buffer)) { return utils::CallbackError("buffer value in 'glyphs' array item is not a true buffer", callback); } baton->glyphs.push_back(std::make_unique(buffer)); @@ -277,7 +265,7 @@ NAN_METHOD(Composite) { uv_queue_work(uv_default_loop(), &baton->request, CompositeAsync, reinterpret_cast(AfterComposite)); } -using id_pair = std::pair; +using id_pair = std::pair; struct CompareID { bool operator()(id_pair const& r1, id_pair const& r2) { return r1.first - r2.first; @@ -288,7 +276,7 @@ void CompositeAsync(uv_work_t* req) { CompositeBaton* baton = static_cast(req->data); try { std::vector>> buffer_cache; - std::map id_mapping; + std::map id_mapping; bool first_buffer = true; std::string fontstack_name; std::string range; @@ -298,15 +286,12 @@ void CompositeAsync(uv_work_t* req) { // TODO(danespringmeyer): avoid duplicate fontstacks to be sent it for (auto const& glyph_obj : baton->glyphs) { protozero::data_view data_view{}; - if (gzip::is_compressed(glyph_obj->data.data(), glyph_obj->data.size())) - { + if (gzip::is_compressed(glyph_obj->data.data(), glyph_obj->data.size())) { buffer_cache.push_back(std::make_unique>()); gzip::Decompressor decompressor; decompressor.decompress(*buffer_cache.back(), glyph_obj->data.data(), glyph_obj->data.size()); data_view = protozero::data_view{buffer_cache.back()->data(), buffer_cache.back()->size()}; - } - else - { + } else { data_view = glyph_obj->data; } protozero::pbf_reader fontstack_reader(data_view); @@ -314,60 +299,60 @@ void CompositeAsync(uv_work_t* req) { auto stack_reader = fontstack_reader.get_message(); while (stack_reader.next()) { switch (stack_reader.tag()) { - case 1: // name - { - if (first_buffer) { - fontstack_name = stack_reader.get_string(); - } else { - fontstack_name = fontstack_name + ", " + stack_reader.get_string(); - } - break; + case 1: // name + { + if (first_buffer) { + fontstack_name = stack_reader.get_string(); + } else { + fontstack_name = fontstack_name + ", " + stack_reader.get_string(); + } + break; + } + case 2: // range + { + if (first_buffer) { + range = stack_reader.get_string(); + } else { + stack_reader.skip(); + } + break; + } + case 3: // glyphs + { + auto glyphs_data = stack_reader.get_view(); + // collect all ids from first + if (first_buffer) { + protozero::pbf_reader glyphs_reader(glyphs_data); + std::uint32_t glyph_id; + while (glyphs_reader.next(1)) { + glyph_id = glyphs_reader.get_uint32(); } - case 2: // range - { - if (first_buffer) { - range = stack_reader.get_string(); - } else { - stack_reader.skip(); - } - break; + id_mapping.emplace(glyph_id, glyphs_data); + } else { + protozero::pbf_reader glyphs_reader(glyphs_data); + std::uint32_t glyph_id; + while (glyphs_reader.next(1)) { + glyph_id = glyphs_reader.get_uint32(); } - case 3: // glyphs - { - auto glyphs_data = stack_reader.get_view(); - // collect all ids from first - if (first_buffer) { - protozero::pbf_reader glyphs_reader(glyphs_data); - std::uint32_t glyph_id; - while (glyphs_reader.next(1)) { - glyph_id = glyphs_reader.get_uint32(); - } - id_mapping.emplace(glyph_id,glyphs_data); - } else { - protozero::pbf_reader glyphs_reader(glyphs_data); - std::uint32_t glyph_id; - while (glyphs_reader.next(1)) { - glyph_id = glyphs_reader.get_uint32(); - } - auto search = id_mapping.find(glyph_id); - if (search == id_mapping.end()) { - id_mapping.emplace(glyph_id,glyphs_data); - } - } - break; + auto search = id_mapping.find(glyph_id); + if (search == id_mapping.end()) { + id_mapping.emplace(glyph_id, glyphs_data); } - default: - // ignore data for unknown tags to allow for future extensions - stack_reader.skip(); - } + } + break; + } + default: + // ignore data for unknown tags to allow for future extensions + stack_reader.skip(); + } } } first_buffer = false; } - fontstack_writer.add_string(1,fontstack_name); - fontstack_writer.add_string(2,range); + fontstack_writer.add_string(1, fontstack_name); + fontstack_writer.add_string(2, range); for (auto const& glyph_pair : id_mapping) { - fontstack_writer.add_message(3,glyph_pair.second); + fontstack_writer.add_message(3, glyph_pair.second); } } catch (std::exception const& ex) { baton->error_name = ex.what(); @@ -386,14 +371,14 @@ void AfterComposite(uv_work_t* req) { std::string& fontstack_message = *baton->message.get(); const auto argc = 2u; v8::Local argv[argc] = { - Nan::Null(), - Nan::NewBuffer(&fontstack_message[0], - static_cast(fontstack_message.size()), - [](char*, void* hint) { - delete reinterpret_cast(hint); - }, - baton->message.release()) - .ToLocalChecked()}; + Nan::Null(), + Nan::NewBuffer(&fontstack_message[0], + static_cast(fontstack_message.size()), + [](char*, void* hint) { + delete reinterpret_cast(hint); + }, + baton->message.release()) + .ToLocalChecked()}; Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); } From 37ff2688b6b0599295875ad0ad843e7f6742c1f6 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 26 Aug 2019 12:14:46 -0700 Subject: [PATCH 06/33] port over tests from https://github.com/mapbox/glyph-pbf-composite/tree/master/test/test.js --- package-lock.json | 63 ++++++++++++++++++ package.json | 1 + test/composite.test.js | 47 +++++++++++++ test/fixtures/arialunicode.512.767.pbf | Bin 0 -> 55841 bytes test/fixtures/league.512.767.pbf | Bin 0 -> 1705 bytes .../league.opensans.arialunicode.512.767.pbf | Bin 0 -> 57862 bytes test/fixtures/opensans.512.767.pbf | Bin 0 -> 3653 bytes .../opensans.arialunicode.512.767.pbf | Bin 0 -> 58214 bytes 8 files changed, 111 insertions(+) create mode 100644 test/composite.test.js create mode 100644 test/fixtures/arialunicode.512.767.pbf create mode 100644 test/fixtures/league.512.767.pbf create mode 100644 test/fixtures/league.opensans.arialunicode.512.767.pbf create mode 100644 test/fixtures/opensans.512.767.pbf create mode 100644 test/fixtures/opensans.arialunicode.512.767.pbf diff --git a/package-lock.json b/package-lock.json index 412083723..a0a0ec4b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -242,6 +242,24 @@ "wide-align": "^1.1.0" } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, "glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", @@ -336,6 +354,12 @@ "number-is-nan": "^1.0.0" } }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -556,6 +580,30 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "protocol-buffers": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/protocol-buffers/-/protocol-buffers-4.1.0.tgz", + "integrity": "sha512-9erS5oyfb5vzLCO1pJfSujA03md3MzaR6zP77lcHobH3tb6Z3S/mMDzCxpRrGnmsPb/6o5RLZ5wpVEM2UMlgvw==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.2.0", + "protocol-buffers-encodings": "^1.1.0", + "protocol-buffers-schema": "^3.1.1", + "signed-varint": "^2.0.0", + "varint": "^5.0.0" + } + }, + "protocol-buffers-encodings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.1.0.tgz", + "integrity": "sha512-SmjEuAf3hc3h3rWZ6V1YaaQw2MNJWK848gLJgzx/sefOJdNLujKinJVXIS0q2cBQpQn2Q32TinawZyDZPzm4kQ==", + "dev": true, + "requires": { + "signed-varint": "^2.0.1", + "varint": "^5.0.0" + } + }, "protocol-buffers-schema": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz", @@ -676,6 +724,15 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "signed-varint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz", + "integrity": "sha1-UKmYnafJjCxh2tEZvJdHDvhSgSk=", + "dev": true, + "requires": { + "varint": "~5.0.0" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -788,6 +845,12 @@ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, + "varint": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz", + "integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index 612a3db5b..d6da19112 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "benchmark": "^1.0.0", "mkdirp": "^0.5.1", "pbf": "^1.3.5", + "protocol-buffers": "^4.1.0", "tape": "^4.2.2" }, "bin": { diff --git a/test/composite.test.js b/test/composite.test.js new file mode 100644 index 000000000..f41cd312d --- /dev/null +++ b/test/composite.test.js @@ -0,0 +1,47 @@ +'use strict'; + +const fontnik = require('../'); +const tape = require('tape'); +const fs = require('fs'); +const path = require('path'); + +const protobuf = require('protocol-buffers'); +const messages = protobuf(fs.readFileSync(path.join(__dirname, '../proto/glyphs.proto'))); +const glyphs = messages.glyphs; + +var openSans512 = fs.readFileSync(__dirname + '/fixtures/opensans.512.767.pbf'), + arialUnicode512 = fs.readFileSync(__dirname + '/fixtures/arialunicode.512.767.pbf'), + league512 = fs.readFileSync(__dirname + '/fixtures/league.512.767.pbf'), + composite512 = fs.readFileSync(__dirname + '/fixtures/opensans.arialunicode.512.767.pbf'), + triple512 = fs.readFileSync(__dirname + '/fixtures/league.opensans.arialunicode.512.767.pbf'); + +tape('compositing two pbfs', function(t) { + fontnik.composite([openSans512, arialUnicode512], (err, data) => { + var composite = glyphs.decode(data); + var expected = glyphs.decode(composite512); + + t.ok(composite.stacks, 'has stacks'); + t.equal(composite.stacks.length, 1, 'has one stack'); + + var stack = composite.stacks[0]; + + t.ok(stack.name, 'is a named stack'); + t.ok(stack.range, 'has a glyph range'); + t.deepEqual(composite, expected, 'equals a server-composited stack'); + + composite = glyphs.encode(composite); + expected = glyphs.encode(expected); + + t.deepEqual(composite, expected, 're-encodes nicely'); + + fontnik.composite([league512, composite], (err, data2) => { + var recomposite = glyphs.decode(data2), + reexpect = glyphs.decode(triple512); + + t.deepEqual(recomposite, reexpect, 'can add on a third for good measure'); + + t.end(); + }); + + }); +}); diff --git a/test/fixtures/arialunicode.512.767.pbf b/test/fixtures/arialunicode.512.767.pbf new file mode 100644 index 0000000000000000000000000000000000000000..a7d345002690bab91ebc8a119f033ac6d71bd8a9 GIT binary patch literal 55841 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/league.512.767.pbf b/test/fixtures/league.512.767.pbf new file mode 100644 index 0000000000000000000000000000000000000000..eb64b5620183b938fc042952e8fd03df8ae37e6f GIT binary patch literal 1705 zcmc(f{a2D_9L7B=MVh~`S{`?{T%EJyX}Z!%sn)U`-v)+`I4B4_fV`^6cn=l~i@!+kyX_et|*q#dl} zbA&2>cDYn364z&w`9f6_M~>TjI=>*VsIVx@mx3Ki#r36>)^K^df~vo*leZan)grDE zn_Zq=^})>Mu3wkqintLrNKfCPdix=5Y`lB9v4JLu)ju=y;IHSmXJ$qW5C`tGxzJ>{ znPpTGS89kSc_Y1~unhW;$i;bwFZ`Pd4*VzK{-3nIe%mf57XUg-e=`sWOxk4Rf=@Dw zDAJC}XmsvpCFRtCjIW7}9h1Mu;wkMIL2U7co(6j4mrou@J^DTO>R2S=HH!(k8A*pP zFyUUeySIf^d?Y35)00&~9fIgY)u#?6C1H=8B{A4+M%B5a`;s!T*@tqHPVB?)&59*s zhf{IO$;XNHDj0^f4U{vVpQCGR!^6Y(wc1Gqa4P z?Q%!nu1+~_C~AaeZxHxk(1ox?7XM1@{U4hTN8324fByH@Gt>)Bo>}ml*;%K7&D2GVWJ)z>nf)ndaEH|;*#9}k|WFG{z zX5cYdQfw51W9OAYN(!vU`Kpf82~2d>^YH+PY1xq09Yew zL3w&TETIaGLjVVmgY5z^PT}wZKu&i`MAuxQ=WFZF7M#~aG`ZCaaA8k}thN-oVh1>H zkA_uRK;*+HP{$+YAI6g)6v(<%^hE*?#=gMdnzB#-K!X@{5RFinjlaZI8+2NOx}Jn5 z33T1AzFwCN7Lb@a$N17r;J)9XWAcoi$kzJ$R>WiEvGoJf_a8-qUWI|NJ%jds3OQ3ND0&Xqw+*bge0#3URzdx%6`x1kzAk?9smO27Q({41j zYdEs5Up$_%E}6RT&hqk|J~ddkx8v)|3Ro4kale1uCg;HxAecvjL_iQJGZ}201~yL5 z5@TOsaP%`Ioh?ae2X9gUn;$HY=(Y1}NV7PRz?xJT?Q5IBUo7168sRbLhnYME>nm52v;48Z4s z3dPtr;H2kCG&V%1vuY}88Z-vO6|G*$ZZh_|T>ZVyPPw*!HvC{dcyCCj8@l)EO>8sn z7MC7J7lw3dyKlrfK0b0oCV)+Ch{b})m{pLVo+}b@s3jjyx<5;U{S$)|m9XTkyO*<( zLiwo!9R{blqG*Z{286+NK_FhZkb2b2qQ@4bG+L!>MTWuFKR+)%Bs% n;PZ`Mm(ebY_27DfsW2)TCAnk{xSwDwz(^cFl(~0zDE9vYW~CQL literal 0 HcmV?d00001 diff --git a/test/fixtures/league.opensans.arialunicode.512.767.pbf b/test/fixtures/league.opensans.arialunicode.512.767.pbf new file mode 100644 index 0000000000000000000000000000000000000000..72bb299dcd4215154c5bd363fe0850bb4e89d331 GIT binary patch literal 57862 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/opensans.arialunicode.512.767.pbf b/test/fixtures/opensans.arialunicode.512.767.pbf new file mode 100644 index 0000000000000000000000000000000000000000..58429df2b0f3f03673c3377e0e0558caa58f42cb GIT binary patch literal 58214 zcmeFa2UuI#mM$n`T!me>tBhS$HqJS_9I(Lw6AYMWa?Tl%lgJq)G9rnb5h7Py>?pv`q$dWeqkOy zcHbbt&F_kdlV9)^V>b_ik5kZtE2=?wC!Z^3es~vuSGOzrCgN9QzkdGY=|hDV3Ua?l z%fw5|u1ZT?d1K)l9UB{q{zZA4C|_AfmIgoyHPur7N+0Wq*4y^D7%3_^0?tFv89loMu8JJ!-%Aai>6EK@H{f*_n zhnr8`SGNnzX_}fFXr7+yFNw5%g+2oJ<6|9{Xi5{EPH$^!DhRWDc@9YrUwmQUoz^hJ z;|-Ok#d_*LmPJzB#n)D0l%ADY0kbi~*XZ@tBS`qo-S=*Z)nnTe%d5Rb5w>ry97fU~ zGao1Fh@LtRz6GK5H5L2ll78M&$B@QL>3BqOYy=bV3>3TJFpvSiHP&U)zINH}R3j z!OG6&NM(YH<{bsopq%!_O_r$q!fMWfW>a&FEgurFyBe`Fz~}atMA@r+Kn?Q?gO-X4 zxozR|bbX4K9@a2|!Lk@nD9JVBJM6CfP)n>~7Ta@zOtrME!wY*?w??ZIUHze9*pqcc z5AFL;41CiYne4gNY=8gk=8lez;z(%0o9~?BDxlJw?e7wuMJAJzeBjs1DVhb4ppq5s zV&LNE@9*zvp?ve`$-D2Yy`bXfVsQ4mE8pM2JwB@Vom|mjnFMLsl*0$Q*n!@4pkp2A zQwO@!fu8h}wj+T=X=-Y!hkwwC-l*7wX4iHPPcvtx2HQ)LJar9yQfh~nH`h3;8vS{s_&m#T3g>(VSPjb*MzF!m5+;z@xktqZ6xr^ZkgMe z>8h_PtsckT5|r1q!tX04CFHcs{q<}^ibp_B=kn%AeR*~3DDSUXjj5ja6zb$QXL4kM z#h(_;w`BU+MHY2Tv)9*GnL}M{c3(-PfmcFdL+8K%y{V!elvEO_Wa<^2npa#*$%xAU z{UrO|Rx)t(3Jwn^_&WJwdOG{fl~Z@59-mS?frft~8vfAnzqT89a=r6pQBJe(`9rrV1D9m#<9yh-J-v(@e%_PYpTDUPH${zN~weE8whc zEe}&Og3QfA^4exUvIaYQCstMlDid5CV#k)d6alE<$O6Sr_ zcYd%xxno%{DlDw^fs%;8+^&_+8{#rt9t|rmXKiq|lt=qzw=KX9ssu`XM++@K)SXy2 z`I*s>9iLd#!=4`~jkJv{>F08Xo2zQt7#~?}If2FjSq+nXE^B;Zev{o_p5U(O7+KIX z!dhEj=gjukru&=8>$^wiRyNaN;cO~TC)mHc`qIFa5T98H>uY*^fSuNp<5yqkSUCHI z!ZPgbY^MJByu@M+qm_h4>%n;xcjUlKio5z~{~`^e^^KmHLc(m@V&3_rK z6vChPXifR!t*v)du?<=sjanmITmLdzfA45bKP;0fE&GpB`_1b8=Jb9udcXO+-)!D* zF7KMj*rW;OF+QznjyGPF2Ile6yV|JsX}l^9?`$Mdb5K{1tSMjq4_0HtFLF@6EOST8 zGldFkTv-g54TrC(*oCFT@<#GEeQ_4|{SzGrA6VJ&R%&;SpOt|{?GP+#U&*aX$)rik zPW=Iu(bQBSR)!~uHaa?7jw$1oraOto+#V^924#G;SF=@DkgUDA@F%LV;mK^#w`;g1 z*FvL~MB2PQkGrg7O-O@AP4+bcxaRaDEj!exo)#*%jzXiJf<`?BjmnjhNtc#oNJ-j_ zlGdW6jVQV9NG>>%Rv_B8H^7zGvR)zX!VfJ=o>1ftLvBJcwStyn&#@vp?;h zkKK&l-2M)C`hkXRAgQ3bZE$34nBG*L6X#{7bmQ19WvigH%I>jw{`SW$z-?NK61@x+ zF1|4KAyxG)Z1d+Q$ETJz1&ofO7+38Fstz$FJ&T{_2U}~Z>N-bPwi(R;O)BVmrP3Im z7P>3QNpYln>JV2jToLQ2W*(B)#oidMA_lrS;KQ;S7{AOkq5J+{7cfkF z6=l1~f_BzN_IP(geN*2QZ+)U6E6DW4(+{2rWnE1EHhXr4wf1Rsq9s4lN%P*-*QS2S zC9MN9EBp=q(j>hxKh|AeUhd=_Rdb(&?6QX5{=vRZYEed{o8ikVU*k^SRW@}Ej7cvj zDk;n(Mfy4FD_%#sKXv=%dlN^Wkg$l*U{6~^b@?j*XP-R$Nfs~lGf6BWy`?S^40+`mqt44$dOK34?lPl zE9rAQK7gCcBMsRhmH?9%b}wu*2ReHv!CnE#SHlHCxoH8Yk}*3Vn+$PG4gh$$NstPD0*)^*K(0=(K& z2LSKpDZ$#z*u?VAe0OQIy^6ML45fK=j=L^cob0L~`Wio1F!YSgt!n8T>~F6tN)E78 zxpn=uk!xr|R$)m|4l&Zp;@$nzCvPa|Si1U$MuY`<*cvF^{~mYt_%7B76Gk?gFhIAz zj&=Sz*oiv;N?}Oj?}0m|4ggf*2}1~YmRDkcr{siDNc+o#@ju9fK}HkCuk^kzt@mH) za|e*n(+7H*+yN}~5=A^OZ|R2a;~4cDA@2rgCzW`v>7K;`dclRMLi4_!G@} z;r8$DoDe-yOl%f)gfAGcPY*DAby@tJj-lix#-|kowCO$;3(Z|(ihGuR!)nV5v)%ng z=0-lGni0XqSY4{0$?l!X)`XlE=Fc2@X`Cwv!JAeXYW_moHLjG-W;0vJ0Rdz{Q`_=G z%$_Tn_@yC}vzZ*=8iVlEcu&1Yx7CD}XI6}>A?5*fGkJaG2ay5l=VEyFTM?#u3Ynj| ze<<_wl~rgCGCk`s^Aq!{jgEFs&y02!g`?Wo&e+_}r`6uVFs$}*k~1*@`!ru+jXy}O zZY_^<64n;l$cdTB{$|1&LX>?xEY*ZH)E)G+lQ zhrjP0-w%K*$nEb48h7@C;Kr|<2{96^F;7DR8GL*q{Z~+;9bzg*MuB{i20#P+CA!5_ zU~C<{=&!i`@A?-}d(bkAJj3XFP(7)3;uESzl?`vrHDx5_w9Nkw)x!$u+|>bEZOaH3 zdmXkIv!6JOsae4|AHD7hzE0+~72Bq^u3tgJXMGbzknU%|vX zfl}YzKRi6p)l!omX{+HJSKKkV#NXNytjv$sCORAYXEx1jv!}+!r{>oN05-FYF74+o z^-wFTYFp@S1=nob4)(vb+9F#ngk4NG@yWoBudPC3+|s*xFlIx&_8$RP=Kf z+q3+`wWhYdduAK0ULcs| zu%;)cX4qS-u7WUY5KJy`cmn?F+(buNGG5;)wy>_HliuIg(_B-K{SK3shM8+9VZdvrn#mhm*OX?wV9HzSu>H zuvNYWoZ=%lHQb2Q!GS6w{sTrmJah%{=-iT$j3CRm*D#tS3?ElQL`0yy#siF+44jQ0 zL815;U0rRB zm6Rk;Bj5DeKE~n-XK8k{GdIW~x|q&hWll}ca%P(|{Q-TQ77R7kQX9JmfuD?;h1*+_ zo}6A#ni*^k%+-l)W^Y3oIWaQGLDMFpu!Fh2GBem(ofCsMG{7g8v<hEmts-PccVtEVLT7#n&{l94aFWUa>(JQjh^yd`6k)uk>l!6~k zTF*I9#K02%3HC6tkH{7iwXnZ=@$Q}}G%=kE`wM&?&(ubinDzyK5zq}H23EG1*dGwN zCYDoC8Qz-4{??@Xn)}5kLxHt_x(;0=hS;f^Ik~#JqV2$RT@qZmSiQfasHU!NBNDl& ztG|2m@WlCRH(u|G-rcx<5&F~N^AaL3k5T#a7O~+sVwOBSsnj)9$CzR;?yC}<)r3!f z^v-+m7ob5IVf7k|Gr-9{F%)+Q^A|s2Kk<@8r+9>%ZaC?E9Rs8L9~hiC+`-|Av6%M_ zP#kPkf52db6LP@QC__9A+>cLn>^!~jjvo{*9L9ZpQ_VgsB|R<5MeF{xGZ!A|`=nEw z8_0pi+R8sY*D?qH)_iY4h?l3WmacPfe8n)gw;(+|g#Pwdv#x;CV$~OiJF?^YtBv<8p79jeTC5p2|?DkQKK_9N9)R~D^t9#sUX%I zpEEHs0kiR01+$Rcrio=fe;I1uKG1fJ%CGP2>+7n|@R7Qx@Ij=mv~UMWP1)<>B$f2^ z3;paHw_j*w->AiS8o0-&0n@3IlIfS0%?D*X(YC{TdExDKzq$dMNKH>q4R<{LK+ijk z+T2{9=_`yunV*N1LwLD_7Lh;h<+Vqcm4?3ft)2-o*wCegSFpe6(ubH&CU>ye9c2IT zqOSI~AwH`q-6x`CaA}ddHe8+H43{%3#vm=5U=@^ANo%YxBZoU_JBGxOQb{pE&bsni z7LM+o?hdB!pISS+6*ZBGh1ku>Vl#gsfR6-%S0PN0@iWu8y5P%yuBe%SBX%i7h zkbu=#-_qGX_i?#1KNOq7W=Ce`Iefv=P%R0M2?TYX(<1@gsTa=p~?`Nz14pV)yO^qB^vV<1dT|1^>I!Pr!{;6<0jtZ+-Psyi*INJ8xyPDT9t3F9#87D6 za1Vorns&j-c~$lGHThtE7DU-ATZN?8bd1c*j6v&;0#It;O{yMP+*sxC7sqxGP-7il z)V;L9pmz?t6&WIqah0Rn=q;GH z6AQ)p+QV>9HU*z`}M>xk<3BT5ZM4kFLLCuKGZk)wF)GoSokjojMRW=Rt zFo8)>R^!w0o`GAv}F*wX%8(g%2^9_kGUEC%E}gJ zyonrQh3ac7<6T|jEBxW=Bv(}57^N2EQ%5n6Hmb8b^TNaOx;Se9exo{2AH2Yu%>rB{ z2G!9r1^bp6@Lb0U>&dP#)iJ@_9gFI$rVJnC#}*8hMxpvjcVQB-v=1W2a)MBOXS%Jr zwsUr8x}M~T>hr7f;}c6eb6rKz_E2Zfjm$$cPIZ?jdl`loLE}+TE7lhg{VkNdkp-U) zjaZNx>0_yS&m39sK3E&t8LHen1D5;=u;kA{kPd(;YshN5{9N10#V42$ZJA8M*C6{#S+ctEh&zE1_vl>bAkKn$k9+`z@=hO-Zp^voS#zI8Bp|N7qd zxO0y+tnh?bA}Kj0(9K-!;Scv=ZXo7Y)YGVC*@^zv@9w>}2u`o=8fGx32U;sq0xXnt zJ(4O179skK$DgNHB;oZfLi5|_KeBo|2bkOQodpC7hgjgr%y*ULly%H+j#R`sIVDv0 zjf{iUmIRXqvmwpPijY}UUY!>P(@Gx?tuKb|gs8ZLa5ob%CBrc~zqGQ39Bd|LZNS!b zXbcD$PSIsU8w;)3es0NiQy-`5$+7A6Q=cd6lHKj1OX#a>BlVRH103!^NtCUzUpj4i zV{K}TvARChkQHe1&LN7@GPVG`nz_-If+$B#c>~Ybyt+1E)U;OR#dv&>2Tq227?BKI z8ge4R-SE}5lQ&-K+PDV*izd*`QupQc6WH8K9mBw{Q9Z>jP4+}(FTe90@Xqt#dBCkNSS-d8gBPb+U90^ZAT zXH`1Ha_Hle%KKOwJkHwYVt-|lhpu&4LFXLCdSQK>Z_f|4a7nBg-=6P)R_#Kq8tdSd z*2vl!t4a(6dvBz#I>9L*r(+q=)Nr5hyz26zOoA1fg4cTTgGogdl_lw64q9*r8#ZET zl|7?lbZWZ4v2SMUqF|`Jwr_#EvC>dy__QLN|aV1Jv?-wO=F)CMNI ztE7lb3`bt!TN+??H1*I@!vfuH^}u(0@f8>y85DA)y@}S_Czp@ozP+nz=IlcVa?zK+ zcliu#mZ9Ch#a6oGaud=re3+Awa~jIYdm46r1Og$@N$2qyp$K@Bus!yBxoEKE3#q*SWkl*g`9fbHW|J{OFCgsf86p zFORNq=TP&5+D=`5Ag`bT?kn2x%(74hHrdHDXP=_gqJq}ZRg>f*QaVBfokC($$+_tv zRw5Zr=90XYxg*{`&DwU5Gs~DE8k; zbfI>kL;Z&IUKbMW*TAj5*M$(X8=UJ8PvJnl;<(_yl9E}MmgP&~WN#~Knb^9zyE$3t zzI}Z0Yn<#|WfRw+sAN(y0EV_Y@|VB9t%6XbQs5X><&nai_2kbfn))V{vqH=}8Gn>EnYJF&(ctV(p%fB^a4)%lL{+(O#K)?9O@ zpMiUF?F7WA!^W_plMS1=a4X~~*bU^`L4-pen1ID`T$`UKs^cBO5ZjI8Vqg+aQgu^X z2MXmXX_IHFMAbGdtG0V&hRGP}sLcwu*D&!- zts7e2Ue_{d=*q_`!JlUsDlH2L^@BNcR$S(H9g?EB!?ms9vM3)Q4D;x)yY1-cWOETRNTh;E zSquBysv!X+NAP!uEg$4A_0cM+JquftwaIR#f!Pfc{8a`t?apj_L4=K_Q)GVg$N~>7 zfgPoZ9{La|7n4)lMjz~JucajVn!UdA($IyFOs0S-mmKb8rhMo4bp>r}_W4VRWn?mf4h+WgXsaRoGbQD?hp*3TnsgH7s0D%Xvqqrn`)9ZGPt2`uuCT`Y=C+1w z$JRKr-9=G$e&8|QSz-)#H&tUQYUmrpfO`Nvr?wD&r!l3hpxSOipH}cD9oD^_iNU()^v$QhPd3pQHJI83S8Rz%Zr}*mmL*UV% zjZAJ$Yu~OGugd-KEvy;WUK+ZFCS`*=S)@#?^hWNW8oeGx{+*w)CfRU90 zdvXieljk13)ibw)iOB`H*$TI1pJ-a+3DL4BkFC z@19uEzaW^OhPscFspfNtC!JfL?5L~C%*cohbk>7?$LdOVK~j8XLsMCHqyvmIHh;LO zvZ8U2IY3Lrd!Zva&AO{|?{aJY<) zf^b_?&(NeS5~YSppWT|MO>*%}%&%^%Oi4%sd<1=daDK<=e0N=WRp->tGvI^|%4uP& zFOQFmFKn;&mBu)_#TK=VKqnQf%nedA{Edyg5^}5B`UVHOn=7-z9NsCJx`!m@7M2v} zWh4eU=sdmsO54IcC^RBG*vHje^XWy2ChR^evm-5Ad-#hcynam%xShB!8uFd0;vL9^ zvA;QOT%1f*@1Df%Z`AOlI6t#j7r)uxv<0D zYAH!@m1y!c27PL!Z+fOV-D|Jmr~4a=d*`{VzVZau{moy|(c500 z7GU;;637KP3I4Y4zEDL#NT9o!+M_QOVrr!Q>dp^eszvq5{mUnDUn=Ltx#Lne+?Oi4 z4KWxv+?R?%U zct>Q8to2f8^ntm}#qOd=Ymx2V$XZ{X=&2@o8;T5(#!1H7>SS|vusO0q1Op`{H4TFc z{NeH#JHT|NK26l6CuY`7W8Y#2{@gWoe?vu8+wAr@tO>>e;CSP&Fh<8%o9u21qWnbW zHx181RQd{QpeDsj|CzpfR89pJmEK&D9%7?%Me&1cNPH$tCIuM@fp+hI`sV5jZF4Lt zo#5kQuKrkVcdb1x2jsa=h&(5C`Hi`66oQ{^)o!80QAs5TdiFDXB1Gi@c1ENM5kxO! zSp1tSXD~Y1lJL{+shJ@V9#t&gzqas?7goUgrsn++9xEU*-AL%EI|BO%vO2OB;uE5^kqqoKWRkh==G1Y8VYDOg_O{>*C0@-t8f zrO=mGmS=ilz-ZnH&TVY(YHuth`J2DI;1->hol8!S^|w;Hd(yzk%RkWH+sQ=j0R*KW z^4y`v=K;zGQ~dLuDUQP(l>qs$UX7DFa#;fC1DVvf=day-A_4Y6tkPv~Nx%3$(IAM#Ba~Zop9hf&AQmk* zrJ!Z@uS7DtsM7wG)sgmwuF3Vk7RgKk(!o4lUzlCygOPzDqepEBX*msjjHTt-!I5o5 zv3!5~Yj`D*bS$an%W#r^!rN#t0=qX%=S?q`V$_BQsJ^=x~cn4FBrwoXei3yLv}Q5iY!|=H($S zY90w4uWTT}0R?g7;v(5q;k+f(WRjUG_v0bL$Yn>pCkJ-)Iv z*_iGFY1>9N=fI%Os~_Mk_D~4kX^qV7(aOZ2i0qcWHfnZYU@l}m>7|5aVYYEeRF% zC-_=le4*ve1NyyRgl@lM7KC=D1Zsu3=3>6u$ABz$booQ z(*;Zl{$dA>!5b)#bJo0jUeOGF=H_Vc$}D!v`4jivyTn%v@fRjVw;ab^cxe_yZe?u% zwFf>0r64?c|2;h1kN|cOu+g{+8_KV4sMtkNx|aCB_i=fD4?>MjKmOpIN}b?u%);Ko z1QH)$o_Io)PD2s7ayKzMN#ygj!+HwBi=qAJj<8|&GJBk9)8)n&^U7RIw^Bes1 z9mMqlusple)-ulq*R|1;Q{cl#?^>T2;eyZATkpXM+$ANO@cj1aW%evY%D8CU`WkoU zv7SdlVSPtub14MWpj464mtULWqf^K^8S#GBYPV4^)XCd#4DIp$0p2bqY7Zf08y1O) zgGKK+N*wT8lsG`BurN2hFBFy+VQ&?(Ut#I~Uc_p#!b*CVS73)4RoJz-+*=aoWbB(# zJ0gIXpRC4d{zxqVGb%QOoaUL0acbotm(ffn*r+_xc8P_^AP&7_ezCha#zp(l1qBoq zG|Zo!;0{$2y^UU6ICfLnIs}4>*4L&Qvx2RZZyZCp4uXRP-0a28rKef1xn010qGrXc;} z@&Y6%0txQ2vVBZx@BG3Xdl=F*y}u)aJNv@KFQuxz4;-!V#BZ-0!hLgF)g~w@w-B6! z;m%r*P(r;kk2Jt22@4PLvDJNkUJ5nqabU8bW(7at?~tDmNYYsE7yrdb(m;+D|J`F@ zj!2TE_pv9xWK;fI>;FnNCPtdxBS}wSBx?~#TCgxGOvSiMlCBokcS+Jh^0%9qBrRr# z%HC3VXJBRt-VEXWxNqccK7Ou*>MZm-p_OG$oquf|Q3$Cv%M+ZT4(ZaOYng{20E^`MOYm1X&g2Cjw z4c_JT`Ib`fmY3zk`Ix*!1dQ3{{%Pj)P)kX?n+~coXSh7xmVnusM=(eAm6eG;`t%kg zHH~#Z_4VQU!oqflyr(9+q55)XZbSm`*wGFM)gk{R9BnJ1ukUuf4WN1t_H@=djFQOUq6i{)-C! z#VQl408_b01@6(JE+AAv6fqq_8yGN3FcpO6*EjXfqfE07N;(Dxh}AQX0P1; z3F9rYSz~QQ3GUj5kITKXjwtTzXsV{fd6~Y3#GcQLe3Ns@842N@#&2$c?Q>Jb0w3t_ z<6@=%`sTNw(jP>j43H7!Z%{^*Yf9!`;HV7oFjlK%wNtxxT+EZyKE6NvF4F z`F(gKZ;Bk=h{pEib#Q)HUmy6rKRkMD;F}5?;F4rd?fcmMqoW7zSKa*_10_qpNa1Hj zc$wgdCFSVz%ZuV&!GIqb;VzGi0G$u6nM@vs$%Jd5@6ymcv?`*9eQ2`qy@^3qS~lWm zvsC~6&k6Uih0p)YBq+DFqoWZPj7JZ2+~cw#-ENeV#_h{5ja;Gs`#FDjF1MQz<%k^r zkj&po%Q6o+3Fm*K7w2%|RTP+)(dEMB6XqN7X#Q7UezW@t3b(|UjwZ_J5@wGifS)2u z2L%i5)wQMZ&OtdHOB>7yG?#gjYWe}>M4T>P zawP)a?KtQk8Oi^6NapXPW&iGwT@Wm6Sc$+J`=^5nu~Hc899q~1+d5QW@_-6|V{2z) z1I;9%l-|KX*5=UQAkbobQ;JI(p;B6s7wu^67Y+npku~wlAt!>%K(pc2VWk z<)?d<*Z1}8#jm(pymN^l32#eD46q0y*EgW|G&EEuxxo|6&Ms_$-$eL0LU##zoREA7 z(^5v#}1zs_N{6-&fUEmKxwetQlHZ<82Q0v*@K!__W5csb1F4Tx5rYJ4WYH zThaUIO~r|L1GliWEa9ib_?Ri`+Ifpw*2zfu(i4eqzbSg^UwzJBeT^;teL!rjgC+&z z`{g;QtID>aC?8moH%dVK{bOx=ps3L$i9BDfBVoXoYKMMEKOpc=V`5OA6qfHGBh|NfX~l_6s0I! zW3>;vzqJ+elUj!tbj__TEv`Y1QnXdk#}mt&d;14qb1*PKCAz(}^h?MrC_g1BjM)AZhm&Yckz}t?FQRh(^pV}R zRl>^a$5+Hp4-gV6*814q5}ryt^@4pZyhk+qf_)ujhe?DJF!~>Ol}NIQFoTVC!Ojk1 zT4MKLxWd3Nm)&2P;4J(cz#ggwIBP?->46qP2`Ra}cZok%pGB}!7fDLG<~Amqa>E_9 zeiBM3?aZB-)`A#68%>d z^BaQ@j+;y_Zk-sZr=+Hp(7EhxAnntbw7++TS;1-cWscr4-Y^kOs`CBRjiE-fCCf5 zyv)_^$iZYT{bOBV!C~glCmSOiYItmClhK^Lkpm0_&3y^~XMZG|}CxqjO zSY3Y_sgRZ{dIlmvlz1wmc~1Ofu+*ji-USn95+ol((kR%2aC++Md*-;CqqLgViFG7} zpz)iz}(`r33)#{~E@=ZD7FAO#>C+oUuU71YxQ`r9gD znJJCd0LUEnE0m0=2tcTca#6AhqTl29R%q)&@OQNLBS1thN?L)p1oxA07js4~U0UWp zL!ilD)bsn)v-PX$L2T?V)FYT5!8+uZ>JfI+FVvGGlzyQeQI`fU$$Mi9D|172HA6Ei zl-CS>+acLYcb_S|eDUP=ttZbR`mcV515%hozd7^cxgTVY9X*BMoEkXEqcmDHwPJrR zLE6mpoSYn@pQKb4rOylq2*6uGXF!DF^NMHWs-CiGWGyjT{0Bn>2s?ni ze;q?^2=U3J#7Mk1LWN_3AXy!T-{e$)uZEJG0(m8>zAojLBc)Z!Zpvv-#XD!32=mk(9YK+S51to7uJLzTOGJYEMBZlJW+s^UM1A zpC=oOD6R8b5Y&OBOYP)PaMVCTw`gbtL}x`(SkcfNunLg}5YZ%-!gfSFGyJKfC}bX< z0rTaFo*}vvhHOm|cAN=h??DQNU*U&_Y+8s*47m8R;z@8W#|BL8qC7L$aVBx4w3eYY zl&cWgwFY{s3YrE6D1d98w>Dazg1`FUr=Na$c;oz?pVX0bd1jafCt*k*|MuH&Pkx0v zicr*=t`1rSIl@J6w=-ON192j`AK!})z0b3&(5Frz#+h-Mo7t&DdPiFiYGkdA8R7qrn7 zi5A+SG_dtj4rmm`Ef-uZoePKd?w%MHopW{6-8rGH`H53psiSF?{5XU<>e4 z0GU*bAcX;PL0=G{sA(~Xgusi{4F-yveuI>*|LzHh0KaH0jCa?%cONwV=;n`+B>-Vm z6~y69UR}b}{WZ>)2!7s#Fb_a65d8xcO9W&%Tj{^T(*I#1h&hzS0=P~f)&B2DwO{HL z?!#Oc5_*LX#C#VLdcAZQ2hX)nua8b4(UzoMU&>)3NxiCFlR|e%>Q&>`S4bqOSMYu! zk)&R=?#LjKq+b8vqLc8A?0w((@@^!80|70EmmpRw74K}WCe$av27*`kOYJ!+3H}yB zaL^tC*&&7hNNYoRqN`A!YFfZ39bOU4GrEs2$=FRsrt zlO^i2vk>|uS?^m}?2@cEOl(il_C9a6p<`jKU$R~~#A8X;vzizi;LjF)KeRrai?se* ziF!)y==$G?>PS1GxvjH8#8#x5c$CNsunn(a&h@`erCG?fGv96w}jo;0IRSz&#akv#aVe9iWEx)L3 z0eB){_XCvWYvc*i6wm@MI{eq@VgeqCzh8t=CL$~C~2auJJ7(UIO~G>K@lYyNne}$ z5sPU(U@(ZmzK)7G$GfVQzENplFyvs6ANnsP(ERj$6+?UQYlj76*dO}O%UXSxwRTD_ z^RUeSDJ}a?QaI^z_c77{(AMlB`0UYzuW?5%JkhiO-asO7dLaJHN?ZQ&(Q{9=?StYo zi)-ua>q@f|2~K(nmmX?5hGtX(dthb;qR^_dBVBc$C|d<*)(t=^2o4*igJ{SNvsc&i zB2^D^_%nl@9evZQ+%aHD8Ud}KYiWI~u`GvNLK|9J=_`)3bwbD9v08Ft0|TP78m2x? z!trrFkS2J0tSTWcAt52EbbyZznnTH}fL}nWsHv&W$Z20fNpwUM19HdYB%ImPvakVt zM5HuOGw^4D(7!%V9FB+vyuK1hn6x=up5Mh@?V*H1nkzV&ubw)(wlv*X3tDQ<^tF#E z>t9`+7++bK;ZBV&^H73gQ(znn&kF>L1HE&6!Qyx$1dP7356iA<>+kQXu4wP;@2Jg# z(Eg`7PK3mqqT)PaB1B^55yK$x6u9A*Zs1@I_VPe(R!?g^g)6|e5t9f3Z}|s=w*XRj zOV!E;VOLJNqPVFu;Lu2cj+`B4D~y`LorBZQihHq`DIgG`l>;OI1@X|HN+|jMHYVA? zB#$l~#eM%!&BO(T_<{s)t@Glt%>8%P{&D1TB%u&PUG$${P_hh2DQzB_+L#^ds>zLX z)_SOqPp;@?@_C=R+e@Pj@Ozc4!zrEfpPBHkNgj8&hUl*43^B*soL-PL&3uL>Kfnk@ z!)}dJ(I@wEczwkY)-cvMkWK_i8fLKoHle~W2~`+Rfz-9ugGp#iY#Iz8w=MDqO3}OE zm_-PewT0NI>7S2#mf?k+T@V6=MYMr-PCqg5PJ)w| z(h1gYup{YkU*C9Z?gW-pA#i+Oe;+zAurwMw z;I9xcTR<^%ZSle3VF5zy;wxC95w$=Dok{SI$O8v{aM&FMLa+C;!{IK!h0PQ6_w*p) z&!~gB9qeS32Lc|Q2VWz;MqmGjxc)O~CUHvAzm<~tzolh2pqci1gbwV@B|3#A9Dpcn zAgo`YlMhAhg@rTiRZ){ky!z`^$=?q*OZF6~N_G~gO7<71N_H8jih9kK=!--h2Q%;C zRjluDr-W72=fdvOj8)wo46z2`#ix^VN&ZlUST#jGOJ{HY zKrbsu?}OSJbxGK0{}bA1>mURLHjPeJx=I-G8J$c_2=z3_@Xn&T`s(~7Z^`7F2-_5A z#6^gvI3F&$7H8f>*W$#R=o*DphPc>2!Ziy$0|Wr#B8z~b zQic&xab9tVo#JPz2q)GB@L=>z!au^~2ic0c^FEj~{SY;05S-%w2V}6FT5B1 zBrk8L4+`QQq^th?s`y&{<~f<&>wkVP6NiQ3B=ajvW>*$rUc%hUU%@U1S+QTs!O#AG z(a%=WvvLA6!A$$rty32G&;&3Jg56Et-u6k(udc5z&q?q$eFdD3v6-2nwqhX2g~J)g zpMC~l3F7Thfak;%A8e{PN9Xv8*6QXN2&_!Sn+Fgx6HEF9kmSwxou!8lq6Z_s!viJl zUCza+D|`G1+?jvXHO8TU_nHLfAr3``5ALLhLy;Nfggbosl`(R-`#I@@wokyEf_^s4 zDe*^OPQm68u!5o^0zIv?o?gJ_5?IsAi^;?gNA1Vhd;;_LNPl~ER;aBqXa?v88ynmF z#i2S9UJuh0xVlDWw}3PnW+~KMNl8Wf0`ei*3hU&Y(pD5>8D)djA$*cPyRisIZkVGw zht1}!?JW0IruZ13`uyC&>e^Uuby}dcDt3QUOWz`AgbHz;F;)H4vJlFg=Rt#`gQA&4F zy`W=mdy)phV8VJybQak3e0?XlM>rNF|)nhYANy{U$hWPn74}!_~~RNnk?tAOyUvQ z_`|2~ywWkn;0|je^{1D=xuwO)SIW z@5cJ;^P(Ky*+mp~vmgKz4h9{kW%!#w^vdMUOkY>`BoE@P4zPO3`@y_K=01`OW{$5E1b=@`D$r**hQ((dhJI9v?f%n}ww!s3T z6+m+98+@RB&#bKt!8x6%J~F=whgZ)H(a6E(s6NyJA&kAv)!E@rT8OtZR)vKyCI&m{ z$|L-4zN?PfUKZnO`CcA&D0O4}g=q$M4%`E|uWiC|;dD**A{^Ka=fdG|x~|bVmCYTk zwYg!|D6gD?feRralblHmbJhI`_U;hGWZ~lD?+g5Kg-ek90#qU-<{y{qJuH)SM3xFx z9qu&HMIF3@3Bi7@MzHs|tzzjDla^1(&x&)06RKaD`VvbTy6HW@cfcJ3u1G>@H-p1l z?a2=Til`Cna%MlVCK-^w5_0qb-K~$yrq@ubNS;8W#t5fOS{gAa$Qp;8)q(9M+*~a+ zB&x3JuOIBI-Z-_tD*Z>R%3j%*(EovwmSje8bt@0pSP3^ouo zyChaN(|VTH5j9Zx3J#yzILn;>#OeS;5a{au+3ic5<*DwnL^yW-1kN`Dl3zjuJ<$&W zy`^vt;i(Wgl@jG+p?U{QHyt~?ANWV?jFj)8{3Ngk{Nc`F;E)_aYD*TX?U3AK4RaU& z5JHfTBRE{nfKLTLkOE3xMuY>{aH>Ezt8DJ2chSvR7(Rl{hhZ)$lQXxc_Jy!+xV54cN9?QVG@rCbv1eW-YucH z0#z$2P@h9ti$_Oiw_t)pT@qCpjJa)wu*Yh75G$$ZW-7H1&frk83QQ1wRC2VZ!LtYI z7ATV^S{@t>Utjy_y^Xl)YN~uw#g;%y!(L5G@G*L>jZdjn_JwO zo5O|%RGS9};nl%G^cWg0aRnvVdrPtimalZ|@u8wG3W8vDXzR4><~fXZ_5RrfpY-}B z+7OrBl}|8Nkozhqw~NJG+L&rc_0oIv9S(x9mRA-aNe!6l*N@-``Ar>NZ8ZgPo>)RE zj~LjpXD5RnV>z}!d(l#J}XKzn{}CQ$w6=38^`=v0^{v*SIXE~jAZom5N%{k0W@;vk#m zJLlNq&S^Gp6+l=VHUiU|W_IQ#ry)~4fMSZ4Fa%dt`fI7xDPFtjO(9!ia#Aqlj=As# zOEHMJQLa{+`|6r+9`3K-g03TRKSr2?Na)9-^z9K4^&9BM4;=qyY2&^Ce3hqLahvJ)tUNq!b@ z-kRgXl2S+s!CsKz8Im5~RRXZz!`<2PgM|eo$~X<{-Vb+x%+;j!VVfn-b+_7ggNY z2%po~kQajZkSu`VSpX2D>xG4nOABxfS(jQ`;GUM|e1frWVg~l!j93p9bIccvp2z4e z_FXe{2rJM)&rsIiTnZrtN5cfJ=WE{+j5doDa>12yul?2WDqK^Qh39WDGD zfj6-Z%PB|E{Vj}hag47X8Cl|ie+dfu3wabkHo;``u@ktkzaX=VDx#tHfN4Q% zUxvSx$?k?;vGCd83?Y#qqZNFAVxXmxG0JTO&XB}ZKs3-Zp~SzCpoRH)y0x|*{1{kg zcz#U{oG(MX%{fSeKF<`&&cCz>&Tbl6hPJ&iFk7p?Wp} z(J|3sUe=l>uECKpF=4KHKB;9DWrd_*8!t%y1pA+G8*mhi4tLk2_{J2q4vvmO?g8XF zSYPffj>v3g2>2*D6C6599~HokJ+FIZrjHI;5Fm^L(gZ<9PB`6UriqdfVhK&wwYb=k z9}4yi3g^S?!m*%J+haA!o)LvT9P9v5I2CP)OQ(ccBR=^CWb?BZ`IQ2V5cZ6veL~}5 zjb0e;h+I<`tO;u(b7#CJJ(gJ1v$Bd#xr7+mVW5LHRMvGcJ~Ep!d=1Tmb6ch%WBS;{ z;`U-M9F3)}?G{_uGKS8ggtSjpDgI`PibnX@ylQk7We2r{6lAS-_r@C|x6nj%v|KJJ z%EwCM;rHL(P}H@;&Y}$Vw1c!yXN949h}3@qWwFUP0x4K9`j_>GS05bQUCdPPoe_oX z8GA&L68tPw?}S0;5>Cbbm2&+mU>HK9IZmK zXoIt>>ySgJuPW8o=)GfjW@Qr`aMDdHNei-3eWvRe9G_8GTvU)6?Qg5`2%>&1oP9&X z!wBB?#_xVQ3rm+cD=Gw$W}==62@%lALy&nQ8ZsAM!;%`Fye~{YQ7YUk!iEH*HX(g5 zoWKOz#g`Cd3o*57*x9rIPsKZ{--7)rm<5oZX-2@Gg>GQ*1e{10;pkM@mMqM}>1pBM z5IXeDT}}8!W7|^`kl!j9I6=o@W`o~6#~BCo zD?OAj(VHV|z==N2hQr)^AYPEco`97N&Mp8@0#bcJ+DW*mNp`aeK__{Gr`bfvdLvlB zw?k=q!ON^FBx68gzr7$*bgb&h5|I3~cx2c`7@E&z%={`L_#iGe~~p6Z)e z49E_U0iOSW+SfcJACMhDud-T!4Nw|wr+h`~@HGgHEg#<8f&ohDUE4uNL1^9k=EQv+ zfM*$-n~Zu=#mLsj#Wr$)@w4xLcy0o*Ld%Pb;F_#w2xb7MiL_C=bos3v_>%<;2AoOI zw#XjYoUBc9)4X%{y*uPS*&G>xoGNtgY|qMkYnH#^6R^Rv+ZI-PdRAv>qub-nEH1q$ z-1_w!NMz8*n`>#AUuvJ>_d^=haX^6ozxKW}tgUNZ6B6T)#A&2CxPR_YA_s-n;JM(Ae*Su>Pwd&vtewCfY2O?y^i_WeA zCIUg%HLMLnx4Z_#3-1&Oyhh@%Z~_PnX-Gj34kUvv;c1wrI3J1&oEGLBMw&5R#;(4Y zD^F_*5A5qAYqBlDLoCzKpONbKOgdo@YNfBw8##FK1;X$Em)o$5QND3XpAif^+VaBm zV7u$MpnT|>H7~WQV|b(w9M3wq0=Rc}Y-!)}i&v9mw}dO|kk71^`R8+!)R2=Ebr3~4 z*IHd)7U!-{3($!&MXp9%a(sX-zWFoY2VfiQZ~sd_*Ur?EEXWBz8IFTf)nqP&S0@@bOHVnpnzr+tC0kOco@h)$b38;-WfB0HPOT7D?ET^tGiH4B!e}82oH3 zt)h5OSXXc+!2gNN8l1rTN22;5A_!%1ps8Hn8mvs>pj5tr`cRRI^=hgiGuYn9ElS+E z1WzxLwU#=;^I>$^)H{KAhkLsbASqAqHobgQ%PugZe&Xfa%!`To^guhUqo18IbdAVw zUwrm-p*=6c)c||&Z+;=Iny#7b3M5FDqAOq-ouQsAcBWY@1lX!aD%~RW?3h;o;%gjgx?JE{gR+XOSgvO54=V zIW$u;`edc2EP-ov<0yHi&(G*N2Bp;u0VB9F+22sM zm?+3W{6ycWC=nC&<-@rJLP@c>DD$Kq{gPaj=P#|TJtbF7jCIaKbwj{qN9P8tHxEB= zhM{#p5~Q6T$nKf0OG(H~4zky?_lBOL2i~FUJ$1EhmGSOpwT+QSOAgoQvVqOD=_!(Z z@%axVc^b!6o51wCf!WoS30zg$_Ysn(ahx%92}>_+Xm70rW&s%sdVF*7t_u)B9%P7s z>!G-)q;d#Jm`opkUyhT>4VqprgF-;kRJIC|rhZ2len%31M+|;P25{+h0r;KSe|_rB zZ=KsS@5u=QUq`*&lU~zMTa+ANvunn?C+4Sn%Hr5N)7`zY0ry^WI(K`no98wL@j?M= zhhm}|1|E;Ljyy&oldpW1=jRt6Ei6CT90F{}_7wlRwzdikB7li0=67_fuf0uDnjYqA zB%j>+@rBk0nId7Ji}5Ad%nHR-&DDv192aYYOW)CHHRpuKrzZzH-qqDO{mp(lr=ay= zaXAd5b3Yt}5F;t}V;ZafNWI`))f^Qy(YrE(LbfvL(xj%u?p685f+Wgj&suzA7PeR5 zG*)0fR-j_>O*~tWEk3L}r;v=g(Z?%2r3qZi8%Iy-+6Sgq4?bM)uM~x{%&#BSumbW1 zB0obU6e9FA)6j8^F6mx=wDs%}kk0c3?uNI3!46`e-l56$CzHU0u|zI^8pwmhrM10l zz=RRF+Iog0q{jz`3Q}q(eueSt<(rUG*-|WUb|M*yga8I6*~nyVvcCsjUle>wP&)8r zOuSwE6{D|yN99hD`&iUsL7iX7{xl;$b)Eq&ew$Zt@jn%1s zNp_fvK1x30a!Bq(TU~KRILr7VYCb(fV^ZLX6-Dt}k^T3@$s5M@#2M?)akezP^!@w5 z&LKd*1TzFPll%8NGXh@+{hzjHrmtO6BnouUk{^TkEAT?+)p$HODKu(4;Lr>Qu#9OI5nc~ExC%%i7Uvw);UmMBoI!hu3IGc>-o zIoh@en&?C}rtI0cv_(4iXzm38dSL`-w=O)}SYBHF_2qO|2}0c-aitx@({uAPqZ{)r zSwU9UZ2aSM%Ig{$Bz5C6Jq3JQ4P6_r;F!eZ)3jM%Jn6fWL6 z{^^mQuH7=YecQ?-z>jGGn4DcRgA(OD6_h9gf%z`OFDyzJ73||`s(Ji`wk4aNTu@S8 znwt{IvD89DDki(8XJ~Y&vpzr8+w3kornqHv?ZvZa>yvE-kUM@WANmiz!J8 z7w)8y#|zz6Mb-Te*SiV52!e_O<)R2Ua!B5#l^=N9sxkseG6lGULUnD}JYj4oJGOlI z835vVr*8l>)D5vh7>J`)iJqT)clwHsIhQ1c&$s1-IsnD>ggz*rq^mM&ZfvYT?v&Or z{bHQ*cN1@~El~3wA~GKCtanP`twnV~GUxt9yt%0VT?Wl!aBRQ4y&Y%zm$tW4AMky9 zNNttA-b*m8bO2ymI~3E{V#+MF4=IZ&nS!I&?>TdVFau!$F86Po`trJ&XLxcpW}q+) zta5s1?s*D9mozv!+$YJ43$W92;O91vuRVSCVspHm1gE_ct4E)%j}Htjy;wl-+8No% z%TI=Di_1G_o=!+ap44^Gl@pOz+%wcxk-#Q84{+DveyJ6}dJeQFSoTm4iC#gm0E4zS zaE}Fs)N%)gkLhZ2N9!k>z|5u@S(se+>e<6;Mg;0;6=C!k*i9e5qXeKMIz7z~0-2Db>cpwl^>`wna`*T4)oq{eGS90NxmenN62i|{_6L?F;MFgd@fK05?y zc&j99+AX%CYiy({JsMszvP#j$Hx1NP)DJ(#LJA=cz{y9O6Qfg`PoVO=jobwJzzD_r zYiXn@H=J?B+$%DpyaPzg2#*Uq?*Dkh#5FJ>M_eS%ObBJ0Ui*OpQ7w)kI`E}h0 z8^~EYbQobd%@ga7w^=$&d_*;4&`urQa+VI85R_g&?61mJuyi;J;6RvbOb=18bP(Q> z-+*lD#+S2naMPL=pOG{gc34alA3I+XKzRh!W^>Qc)ydAn;35{(y3CN*)X}C`uL zg%QjCivArVJ?$&UNv1l*bTDUfJdx$4O*~vr!Mp6>DM%3!R7|+QNbO*>OK4uBq`Ekk z#U${P*va~Wt%Z{6AlwgcSXYrKw`uAXoD}XBJOIi4HY=@f8h^6dB@Sm8x(SP=OV3wl z7oR*FsS$abXju9tmA8+stglV?OS1Tm+9!1Ec?r2ytsU(!$im%?FMNJh-_A2MF@uy1 z;%$EUJHSC|8asIT^8CH2F{ZkWFL*%xf4-wysG|1I?0=)%prHcS)@f~V&CnS<{t zeQ@N;T}RJg)Rdx9fMsiN;oE&EICtkK67Ww_C=lobdaO&}e$J|Hm$B7Uq=q^h{-jSB zqDPjWtSqf=E%eu<1=(wv`=?9B9?uT8Hg}9H!G=N%3PGCQhcj)ZS!uZy(xv6L>>wL& zQQhQvS3z1yWnmKF#bJa}vQL!5SN4r}6=k(8$v~T>4@b*^PyX9FsG3yrDX{_wFaG-npJbk%2zr5lr|%wok=jBE{5re%VMtbXhv$`Vkck=wbr z(%)ECRM|56Xu2`o&-@-Iv8;FL;q1`B$l~MKjsl?@45k3^w~Q}S7Mr%BcyDu!GkOdj zpg7yf@~Fs-;o4~Z_|-XmJ8oEFHgIWBMB!pZX8!5HmmEmz3*B!HN}Gz4{4B1Jte2P1o;`oI zIV&v^F!f1$d0=30WZ};vu0rAz{r&Lp==l8O)&A00ciG>Uf>v#upmK9$?Q~H}@xZg?mUOPH zEvauLE?m@hlJ@HC?A*fIixr?~GtrhOLQQm40Yy7Rj%d@?P+gD|?5wW3Gs$Ap!@d>hgol_`%PKaM+??vfe$GTZ@mub%t|@;2K+ zbtiDkDD{Dwki)t+5%xBr3%Luybx#m?Z4g2QTc{fB4{FM)+NQ880Ivj)-6vkGj*pBk zJzwl96uRDWiV(Mf))xltL|0i7*W!vPCnjIgMo1-Uz!he%eN5Am6CuhXGT)^sm>Wd3M&}b4yadv&{9S10S(iC@YVN6j~_b)J?-x0$+&|z?YAc`-PAKQ z(6jc6K#Vxl8DQ87C{q8p{F?fjRG(d7qzR?%lXLJo?SdmMXj^zf!qB?_NqYyMPBv8~ z`|O4!oxxixA(*inl=L4-w94LSFDxkyR=ojCYH{feXi}T2)J*Ek-T^0d(vlYrAregX zhzL+PX}tkZ>ZJ7sM5&Y38z7}l&}y&2lsaj>0aR*vw2dbi!s0tZODaU+Iji^}2+*2bet8AvgR-`7Bq@wSx5&fNZ(B9wY=H~9M36+v# zr{;6(AwB*}lK@Y3WTMv*SbWsrS%JDuS_vbi_eh$vzdt{#UmzJjgsOJ`2ii!_$IQBa@{sf;x9I8Hq99jG0Q@3nc za4#?|gdPmU7dtw(hAZ^LBb_g;le)q>)@1fLj`|nPiICkh0 zMa#&@$l&^EdCLeK}+tYCTfpDG0EK1zyy$u zF!f8PhB(T`ms%cN=i5Mjli3{a6TtmSMBZtb7*F^AiOY8L$716wf1~b@%;Fk(rS74X z#kGF}_uVeyG6nbDkNb8Jmu%XXz6E4c4=Cw0oX?`Q(^cD)ql`<;*R zilB8M@O&dGX+Ho&z#DU z>t;;9^li_a+J*a`0?;oL&s+?GNZ(wu^RpqDBI=pDa|MNudpvVU!T2Md zx$3`b&z#HO@XW=sZeKnADfP@%0lK^;E6~mI=8y0HY~m$^XAY?c$5Il23LnPJv`>QrV870(HSMnefToh-4;wkss z1-qdVRsEt74Fr+B%EZ-)TuQT0JnfzAmcL;A~ zGI{;N_iqzP`+Ha4CXsyh{kI8ZPB`Oj@`zRYJK{(HHpzni9ckO*{m7 zz|2`*Uz;CkFG=u0br~^7zBu^Jk!uJjiSlb&J35*w^5Xn#ZYngS(uY4>GjZ~v5TNO? zp&V;n#LE=Nfg=}nOc)dk)ZOl$=E*M(eur~XX{`NBXE9&_T(r3C9 zsDA!y3i?eN=-nFnZ3}|t7rt#t&@98ZEefKDul>%l*w)h5erI6}^vl)s)i;;Mj-I~o z`r_E7sP%t-eR(J}bz*dY3-LDYULbN+T~%LKnjY-%+7glJ>Wc&sMT zYtLelDeR}~Ypb&(O*vtXdzTAQ*8NY0+uIt-vjpsWyB7>q*j1zR(!9)+m=Jf}ZMSzOt!@Ob{}`R4kg=L0J_>P_1M@L?&|1U{FO!k6g3}1&u1jL zS>@8TC8UvBDT+V(pDy>c)RZ)|H`f8sM^OdRzWCeL(&S)geNjrdn~|b`qf9!z@!~H} z*JgSvlewmftJ#H((&3qv$A4MrE{JecTy1FWC@hz>4XjYELdDgY{E6$^ ziNjk`kl<%?V>|CJDw%q{MNvMsI;XaC|6DmdxV{59E@n4Re=g7Z`t0hb>b3h+>ru_L zZ(A5F+<6fKVPq)Ba=TcE6P;O9R$h`9%ib1)XmNA@$mn2err)*}(4F z@C6@Fk4@GkdTh@=d=v9c71f0T*PW@Sc?)p@Z;M^?PFZVQ%(b>BeQS#n+ZbFvwmsuL ziiC#`6w{qA%k3vHQHr_F-npch=v|i-kP4cXr9ZDM zu0E0V{+P-s3aGg9=;7*&f+X*IEr-y|nvQPi&L4h${tlC$l%1DbG4`7*oUZcDpDx{H zL>70ncFZEKLTfaY_Wz*E#9n1(?FE>x1kMCOYyWXWk0hY)t}l>ulrY!(12+C8GU;XZUB!Kdml#l9`1KpInjseYx`6i zRMf_H#Dr9H?Ht>pLh8ES-M^m(Qub;M5>z?*tr~W$yVV8_tlYYO;n+_DsyqjC9um9skz0 z81;0_Jbb#b`C@&zF2xrDr>GN~eziE%DV=(@(Sr(Rg*b_OknU5SEpDEAiL`r9g-BtO z0=DyDVR8RRKcamKv8taV42NE!=@n&#*^#aa(P~7Ya7korUTH;1Mik3LahVp6Bo={7 ze4sHa)LwD9ZW_pNWnHt6pKOg(#xiA>H@koxx7^;fxY|GboOt3DqSoBK_H<-wv%h8X z`6z1072>wE@$Avoqsj4wr*kB|Xj|kKM~9Z5LXS*!fG^y5TkM*vr0dfIFgV5WzBV@$ zqL-I}B>ev3f*esK0cW7%$KeGBM~Wq#Ef2ziIL>CkXi(aZu(`3hl@&*joSPHPwldcF zPL*FFXIQJ^5DiK4i+TJ*fGSyC9!F{S4-OH&;v z4kzbp{k?1B7&US}QCmArICto0%4=vA?O!>c8^M9m~^`>F()?q9eecyiZ@I?~%voB#&`g+sZS5Jn7X#O1ie(xU4)%v!iC^mr&H! z4VoQ11~VG|@a48l@)%@|PPY8rJVv3iYL|*yvhpwT>9{4I3c__qrh#(40(lB-cy*nf zr2^8PMO$(GZ9k1=LoRd;qf;pKORsBMgmXSPS30{aEez+2r9-_HNxs}9aY;dHpuIhp zAIbN3GC26o;SZ^xysMt0tSVJei&NIKb@%Y_=D1-d0^*XAQd6RNj_mj{iKMZiOyooR zGdqW9Ke^-l@VF$ITw9o^s}@JO$a)b6q)OSD(Ai8=adz`ZoMHM2a+qxLk>PbnHuW6S zOJB++-a_-J>cM@7-@~+jd37J@R5$6=ffJxX_V94C(pE=aN=#Bx4A&Am8Bo}gVxg-H z)QuF>AE)qccmd3G~f zvphUp@y~_r;o*Fr5gebCB)~tw(IiE%ypyXWlKe`EBo{!iS;0tpm>sL?m>nsX&h{3B zp$=|>49@p0_pG9xivbjj+R3Ta_L-3- z08m=c+OCz^`jG`-hWxWZXP+$1AC_zCORUzAlqz@yn5$4uMc& zf+SH=82B|m|H;6f?dj?2;pyQ7E<}VK-=mZ8K6$Es@>F!0F#-PI!L+<-$HPh!61fJD z2Zl)^DTpbN5Reay-z<>p8u3tZZb(lUT`d=?CRUHnwk{2p z3YmIZj^S`L^rN;Y$Xeq%fNL9Or;wrvI1^(JVS4G{W;dx!I&2l3)7&&l3QJ8denE(h z7uWm1ABuuQi@@xTp&szuTWP2(8GFU&LuZ6K>z_QJq-pIP8W{$fm!DzppbT0jTKm;S z%Blk@YGHB!#sJqlz^={!{9;C^18kydM7_~{V}yA@{RE#6@>|;1mPr5X_~PCz6fAn) zL5k$?x)dbfTF9cCA~jDY7kmH<)L$%c(f!do10>MJO)j{T5`5$xYU}Cg$`6tEsdtOyg~_`${DO2y z*0Y+a4XHTHQP#Pph3Wbfu8FLF9e8OATyDv_*tgI^$WCc5^P5}B<3RmJI~tQtxRS0P zqJ3p?xgb(H^C<>S)?HO~Pt_q6H9Kt^uP|Y#`(0~pd|r7LpTUkTZU^ADPf&L2+!QjY zB8vx>+X~`5;a8p=fGLEZl>Hh%ACVk!K^gM@CCOgA6r}w>K#`VjaBwis?LO4I3SiR+ z9t~z_7&COn(8&+K^LM$8!t7LhIvoV0F45)qdq{{OJF#~95h)NbCeQVP;GiRvsWX1g_7l2CG6Y5OUV&LH zv(xPl{MoTZZ6nnpZ!2zGE&xp(Z(4iu1tIQtcRg~T>ZppEJ1De*12_)GSKNg;d0B~k z_q!m?>h7q{;yaVl`jyezR9~M|NqheWfRdPz89B|1aJGRss%sE6kwN4$i$m2ZTyA<} zM@vO!2!k1wnUxL{sM{v){sCOJ?d?kk-^DN!=a!0kpz0Xzc~0M&<}7xUesi={KG_zY4o ziyPWH#C#olKZ0Nr?{oH+1!{_TUN*b#36UQsB7a`r+A}CDG?0w}*JlL8<`jstVgu~7 ztpid^+xmK2%M!U}ER<6&%r7i-fs%}%W{fwtED(??w`g%)YhLRjc|TilB2uy%Cl)*N z`F5J7$R!&|g5uu>qsY1VQA%!i4`aN<4<_enWfD=MG^R>jMAooIdH$Z3X7Wh=axrM zi|HO;Gli3?oOmCc4lm6OG{a@c2+yu;8$qKu;vOp`KBuyM1b?P+*A9g`IaN*h{6jx! z-nI7(O-POQdgrt6&uJRldw9EB;AKt}$LGKUM7O3$w~U$Sk&D@_OUJ$f;;lF}(%0Ec z_aZByrbkkk63KPGPtBliNkK{k%RR1AI=MdCU6&n#deDM8G~l6hipZ&IA6cI5D+gyhJ+QZ1 z({!{jn650A(|wKa-i2|*6(%Hvvk>JwW#A?(s;$iqwbweVVHE&S{}Es(7@s$Ei!SM> z$O;ZxRwOrhWDXS^UiaL=ZoS;zzub|_cL)Hj^+;9I+$c0vNPhQVdv0m}Ap9U)QBiS0 zd|GjFel*L$FFYd5hZhmS=U8A8$+DD>)(q*^T?UKIvUg^)-EH-m{Dj2#;E2S;IG%k# zMonE+N={u}IsNLH?!u~(8S>RBb(52l_GPeJvz#Ler0d-yfZPkTHsvPQO-wBhlmc!~ z7fF)RwYjEDp7qs>rr^FF0EL&k;c3E*omEtn8UX6-FE8F>1&4V%b{h335EWOd05O>!g+9g&DI+GO@ zkwLn|!S@aiH;QxX2ZzZfAh~wv;Z$4G_`|V=41Xt7kd8c_nRo&^v`Du*%rIz?7tdx8 zd|}=H#n?SEuYGh>S}61|(fIC)iC0_^=?vHW+L8B<+^`{CLT9v(9oW?$hU&bE+IfWjgDr$}l@;;=8zq3&wd4bXcU>`#AR;35_y5v<$50YPihWtPyG^<>GpqRluR46~- z@4Y|F8!&OUfJf;w65Qgs21^BCyL@y%k_%giG2;KYEW`9Kz#~)3@$Rjf3SUB>*7geE_CQ(O z%bGI%F)^e9^lkG)<}R|hv$HwV-!CaU`+TAYLMV9tvPhq69Ge4_w z>g8lzGWV`~l(>C<9j4+HGfrGdXJ;Cw1-cbUco&eC#ynt=4cW zI0AxjuMjUwJ=e%=+$%4JZRVR=k9)Od`a4G!_G~^LsZHf_QzTO_CL3Y2gh30 z-vzpDXJ<*Am+2L50`3EZLd_q*QjGi90h~}S5#&}~%Bt%sYSqe+36gdY=)|GOr=_X> zkO<@wpooWLe!rT9XfS|dv(^s|LK7dTxTXxAl@9bV{AS}@B?)j_MfoX-Ae8-!H?lEP3)rp*Y);uJ+cjWOMputi7iwZHnjHbD@m5$tC zzl=r%Y5*+Z76p zqqm0(YN4I^$*D$xvSd@XnC3U+w%QSDA3nrAPXLyU#qn`7 z*R=9NZ57bKOugdrsvDYeLtP^C+s6oon5RfGIX4JC65M>bw?r)RBPRJ!V_kMIJE3xL zjeN#FD5JJ(04Ij7d{KUJX<7h$*WRHKQGw38?g`WX5lsJ&WVM7oj)rGHJ*i~5U7W#vK;V}TZ7YhdRu1kXBL4>5rZuf)=-Dx6o9r*QFb6B85D6BCH5%g)inIsHr( zj*1knT>#njkRQX4e31>9(rRRMc)?Gj?FQIo`iv z>cNkV4fir74uKmwPS&kulK`Tt}mGhOXhL_o^rq z1v~241ZGJ3#z&<91T)88c3^XQhCqQi5bVH7Ni*3Ix#ImXHPz6%2!;$7l!*4N%`Gf< z6@=Sc1*Fz=4-a)#B>I@@Ifo_}l$PY95ccNJcNo55f+#)*2wT*|Q@!vZ3MvGuFI3e0 z{&eY%6LN!moQy8qVTJGs@20)gXZ%WP3+5paw{@J}YO46oWV=Bf_^;eh@ zfULN&$0OansE9=>_uSSa>=cl+UEJQ=17c|%Ti>`Gu{bN<=d_Ns7hY>*Z@3S_rF4P3 zLkpBuU!sU!`OrlJD+bfW4seieu2ulEa^^+GMu&33;^LzHYz$n((n_kza}%?ws}cIN z=B3y5kB@Xzl?{{4785r3=Ejrdk=}U(#8{--aH?}~;pyX*Ze)=lKYp~YsJwM>xEqmM zGo;U#m!-vJm6VD_K@44L*9EbIA_aV32gCDlgzd65UqLWWpuQ~}y5n{BXSUTEWF7mg%$x8>sP;zN`iHPTL>$FQStfBw`u{HJ2ez`4}AE}or ztNyB@madHQR~r{EUIgFELgQOieMg@Vp(v{;A0hC|N3D6ud1Y`FG^YAtg9)ES*UD%ZjA6jh94zaWJPj8r+YFQX6 zkMp=oa&CJ!2IpHI1liue;HTANQ-i(E?Xv_wx@-UzO$0;x$j@f*NzYGC)Fye|Id$1O zIInxD4;cA=Cg(0$VyUi<5&Vz)KOsgfs_q%;!e-s*OF zM~3>?>z(>|m)|f<^)D)F9CveQ z8+^~$!qQAnSv>nLTaZ`X*j!(n9AK+q=E+Y;O%oB|1r+oz>9Z| z$ZMOM>0TYFO$)Fz@r*6*UYW(70;F2nc7bWNqw75|B26w^a1+b>Hzw!X@*|nISgiHHdGakzcuh$%ZfIlrh_oD_oBAu}qsu6tlmTJ<23 zX$bioUteBc8E(i3w23I{eK^+IGKTV*Xjjx&Je20;HVw6v2wftI`X3LL#uSugC-_!o54BH&8O7gb3qj}moAgykE1+FanYllD8N7ZE4`Ut)XSOORC6BK&l z!UB-M83I(jL}D($WPY#ni>z(MRlbwAmLufTD84( z>LcPAKY+LUXYkVi#`Yfxj4kzd!sSW$7eUh>;7&GhSAfabL2{?Q^ z0H%v`|0FQ?W>wI0dk*+|QB+8--!wKeBT%#C{D-p_v=!(3K0@xa{5q(R{x6*a^?=xw zK>kCh;EI($u$h2qi?WuJx9ovw!F9R~`6gg5WG9JX`X@3>z7`)UAK3reNK zbmHgB+7v_q!=z_sZLN4bI;`?Xl?|(GIOM}hhvx5lSpVoS{g>B=iGt$Ehl$L#Vwm1G z^Y5$eNW_VYm$#+kUoouIK$l4?wcF`%%jhO#(?G#Xwx!k7R55LU+1kGEz5_e`d0U?; zCdMCMag7A3H~==Oc|FIjZ|{C|3w{^+oN@cGVgpSXBsTWa*p zZEO_MME6eHN`P#h6iR^Zo)k*pb!nx#NkNW6bQ6_UvM=4Mn<|x6|GSD>mhzu2+%k7? zad9x$J^$^`cbxnpqNBro8MiO!IfbSaRlvp!VcfFxPp$149qq18@wdd@sds&5cCEK0 ziUE^gVy2;KZUS*o*?IH)cohH|@tL*Rxm7UTFf9F3YPv^8q}9oO7Pp*2lMBns3Pd3c k-3zdt!iB;x>|)M;dqLOS-r3pSOy@j70VmOb%U4zZH!mHH_y7O^ literal 0 HcmV?d00001 From a0c20726946b9526a65360a613be2240e76ea382 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 26 Aug 2019 12:25:40 -0700 Subject: [PATCH 07/33] attempt to defeat the node_modules cache again, to get new dep on travis --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 065f488cc..99126d108 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ default: release ./node_modules/.bin/node-pre-gyp: # install deps but for now ignore our own install script # so that we can run it directly in either debug or release - npm install --ignore-scripts + npm ci --ignore-scripts release: ./node_modules/.bin/node-pre-gyp V=1 ./node_modules/.bin/node-pre-gyp configure build --error_on_warnings=$(WERROR) --loglevel=error From d6bf0acc7db3c130091a96a6450b2ace0385ce73 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 26 Aug 2019 12:25:53 -0700 Subject: [PATCH 08/33] Revert "attempt to defeat the node_modules cache again, to get new dep on travis" This reverts commit a0c20726946b9526a65360a613be2240e76ea382. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 99126d108..065f488cc 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ default: release ./node_modules/.bin/node-pre-gyp: # install deps but for now ignore our own install script # so that we can run it directly in either debug or release - npm ci --ignore-scripts + npm install --ignore-scripts release: ./node_modules/.bin/node-pre-gyp V=1 ./node_modules/.bin/node-pre-gyp configure build --error_on_warnings=$(WERROR) --loglevel=error From 702a47087399c5af7078e4dc1dd1f61c658273c3 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Wed, 27 Nov 2019 20:54:01 -0800 Subject: [PATCH 09/33] port to node v12 --- package-lock.json | 6 +++--- package.json | 2 +- src/glyphs.cpp | 18 ++++++++---------- src/node_fontnik.cpp | 10 +++++----- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0a0ec4b8..ddf952160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -440,9 +440,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "needle": { "version": "2.4.0", diff --git a/package.json b/package.json index d6da19112..16a931d02 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "glob": "^7.1.1", "minimist": "^0.2.0", - "nan": "~2.10.0", + "nan": "^2.14.0", "node-pre-gyp": "^0.12.0", "queue-async": "^1.0.7" }, diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 1f3583dfc..47a684117 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -155,7 +155,7 @@ NAN_METHOD(Load) { if (!info[0]->IsObject()) { return Nan::ThrowTypeError("First argument must be a font buffer"); } - v8::Local obj = info[0]->ToObject(); + v8::Local obj = info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked(); if (obj->IsNull() || obj->IsUndefined() || !node::Buffer::HasInstance(obj)) { return Nan::ThrowTypeError("First argument must be a font buffer"); } @@ -179,7 +179,7 @@ NAN_METHOD(Range) { if (!font_buffer->IsObject()) { return Nan::ThrowTypeError("Font buffer is not an object"); } - v8::Local obj = font_buffer->ToObject(); + v8::Local obj = font_buffer->ToObject(Nan::GetCurrentContext()).ToLocalChecked(); v8::Local start = options->Get(Nan::New("start").ToLocalChecked()); v8::Local end = options->Get(Nan::New("end").ToLocalChecked()); @@ -187,15 +187,15 @@ NAN_METHOD(Range) { return Nan::ThrowTypeError("First argument must be a font buffer"); } - if (!start->IsNumber() || start->IntegerValue() < 0) { + if (!start->IsNumber() || Nan::To(start).FromJust() < 0) { return Nan::ThrowTypeError("option `start` must be a number from 0-65535"); } - if (!end->IsNumber() || end->IntegerValue() > 65535) { + if (!end->IsNumber() || Nan::To(end).FromJust() > 65535) { return Nan::ThrowTypeError("option `end` must be a number from 0-65535"); } - if (end->IntegerValue() < start->IntegerValue()) { + if (Nan::To(end).FromJust() < Nan::To(start).FromJust()) { return Nan::ThrowTypeError("`start` must be less than or equal to `end`"); } @@ -205,8 +205,8 @@ NAN_METHOD(Range) { RangeBaton* baton = new RangeBaton(obj, info[1], - start->Uint32Value(), - end->Uint32Value()); + Nan::To(start).FromJust(), + Nan::To(end).FromJust()); uv_queue_work(uv_default_loop(), &baton->request, RangeAsync, reinterpret_cast(AfterRange)); } @@ -244,14 +244,12 @@ NAN_METHOD(Composite) { CompositeBaton* baton = new CompositeBaton(num_glyphs, callback); - v8::Isolate* isolate = v8::Isolate::GetCurrent(); - for (unsigned t = 0; t < num_glyphs; ++t) { v8::Local buf_val = glyphs->Get(t); if (buf_val->IsNull() || buf_val->IsUndefined()) { return utils::CallbackError("buffer value in 'glyphs' array item is null or undefined", callback); } - v8::MaybeLocal maybe_buffer = buf_val->ToObject(isolate->GetCurrentContext()); + v8::MaybeLocal maybe_buffer = buf_val->ToObject(Nan::GetCurrentContext()).ToLocalChecked(); if (maybe_buffer.IsEmpty()) { return utils::CallbackError("buffer value in 'glyphs' array is empty", callback); } diff --git a/src/node_fontnik.cpp b/src/node_fontnik.cpp index 22753e658..f9b5b41ed 100644 --- a/src/node_fontnik.cpp +++ b/src/node_fontnik.cpp @@ -4,15 +4,15 @@ namespace node_fontnik { -NAN_MODULE_INIT(RegisterModule) { - target->Set(Nan::New("load").ToLocalChecked(), Nan::New(Load)->GetFunction()); - target->Set(Nan::New("range").ToLocalChecked(), Nan::New(Range)->GetFunction()); - target->Set(Nan::New("composite").ToLocalChecked(), Nan::New(Composite)->GetFunction()); +static void init(v8::Local target) { + Nan::SetMethod(target,"load",Load); + Nan::SetMethod(target,"range",Range); + Nan::SetMethod(target,"composite",Composite); } // We mark this NOLINT to avoid the clang-tidy checks // warning about code inside nodejs that we don't control and can't // directly change to avoid the warning. -NODE_MODULE(fontnik, RegisterModule) // NOLINT +NODE_MODULE(fontnik, init) // NOLINT } // namespace node_fontnik From e83e79f9df971d7d08047d60fe355bb52c174037 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Wed, 27 Nov 2019 20:57:21 -0800 Subject: [PATCH 10/33] drop node v4/6, add v12 --- .travis.yml | 53 ++++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2830a00e8..a7b3946aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,22 +39,6 @@ script: # run your tests and build binaries matrix: include: - # linux publishable node v4/release - - os: linux - env: BUILDTYPE=release - node_js: 4 - # linux publishable node v4/debug - - os: linux - env: BUILDTYPE=debug - node_js: 4 - # linux publishable node v6 - - os: linux - env: BUILDTYPE=release - node_js: 6 - # linux publishable node v6/debug - - os: linux - env: BUILDTYPE=debug - node_js: 6 # linux publishable node v8 - os: linux env: BUILDTYPE=release @@ -63,38 +47,41 @@ matrix: - os: linux env: BUILDTYPE=debug node_js: 8 - # linux publishable node v8 + # linux publishable node v10 - os: linux env: BUILDTYPE=release node_js: 10 - # linux publishable node v8/debug + # linux publishable node v10/debug - os: linux env: BUILDTYPE=debug node_js: 10 - # osx publishable node v4 - - os: osx - osx_image: xcode8.2 + # linux publishable node v10 + - os: linux env: BUILDTYPE=release - node_js: 4 - # osx publishable node v6 + node_js: 12 + # linux publishable node v10/debug + - os: linux + env: BUILDTYPE=debug + node_js: 12 + # osx publishable node v8 - os: osx osx_image: xcode8.2 env: BUILDTYPE=release - node_js: 6 - # osx publishable node v6 + node_js: 8 + # osx publishable node v10 - os: osx osx_image: xcode8.2 env: BUILDTYPE=release - node_js: 8 - # osx publishable node v6 + node_js: 10 + # osx publishable node v12 - os: osx osx_image: xcode8.2 env: BUILDTYPE=release - node_js: 10 - # Sanitizer build node v4/Debug + node_js: 12 + # Sanitizer build node v10/Debug - os: linux env: BUILDTYPE=debug TOOLSET=asan - node_js: 4 + node_js: 10 sudo: required # Overrides `install` to set up custom asan flags install: @@ -119,7 +106,7 @@ matrix: # g++ build (default builds all use clang++) - os: linux env: BUILDTYPE=debug CXX="g++-6" CC="gcc-6" - node_js: 4 + node_js: 10 addons: apt: sources: @@ -136,7 +123,7 @@ matrix: # Coverage build - os: linux env: BUILDTYPE=debug CXXFLAGS="--coverage" LDFLAGS="--coverage" - node_js: 4 + node_js: 10 # Overrides `script` to publish coverage data to codecov before_script: - npm test @@ -164,7 +151,7 @@ matrix: # Clang tidy build - os: linux env: CLANG_TIDY - node_js: 4 + node_js: 10 # Overrides `install` to avoid initializing clang toolchain install: # First run the clang-tidy target From ab2eb76ec80751e245ccfe88f06de696e14ba5a5 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Wed, 27 Nov 2019 21:27:39 -0800 Subject: [PATCH 11/33] make format --- src/node_fontnik.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node_fontnik.cpp b/src/node_fontnik.cpp index f9b5b41ed..c7c31248c 100644 --- a/src/node_fontnik.cpp +++ b/src/node_fontnik.cpp @@ -5,9 +5,9 @@ namespace node_fontnik { static void init(v8::Local target) { - Nan::SetMethod(target,"load",Load); - Nan::SetMethod(target,"range",Range); - Nan::SetMethod(target,"composite",Composite); + Nan::SetMethod(target, "load", Load); + Nan::SetMethod(target, "range", Range); + Nan::SetMethod(target, "composite", Composite); } // We mark this NOLINT to avoid the clang-tidy checks From 5507815d458689d192fa88f5ba0dff9b30a24750 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Wed, 27 Nov 2019 21:29:02 -0800 Subject: [PATCH 12/33] upgrade boost to fix compilation error: unable to find numeric literal operator operator Q --- scripts/install_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_deps.sh b/scripts/install_deps.sh index e11ea7741..105b2b407 100755 --- a/scripts/install_deps.sh +++ b/scripts/install_deps.sh @@ -12,7 +12,7 @@ function install() { ./scripts/setup.sh --config local.env source local.env -install boost 1.63.0 +install boost 1.67.0 install freetype 2.7.1 install protozero 1.6.8 install sdf-glyph-foundry 0.1.1 From d291b85db5533b960c9ac06f2b34550a587c5930 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Thu, 28 Nov 2019 06:50:06 -0800 Subject: [PATCH 13/33] workaround gcc bug --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a7b3946aa..62d8507a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -105,7 +105,9 @@ matrix: - true # g++ build (default builds all use clang++) - os: linux - env: BUILDTYPE=debug CXX="g++-6" CC="gcc-6" + # Note: -std=gnu++14 is needed to workaround: + # boost/math/constants/constants.hpp:269:3: error: unable to find numeric literal operator 'operatorQ' + env: BUILDTYPE=debug CXX="g++-6" CC="gcc-6" CXXFLAGS="-std=gnu++14" node_js: 10 addons: apt: From 133c49a755d22097fcd2292b8fc5786fe3d96677 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Thu, 28 Nov 2019 06:50:17 -0800 Subject: [PATCH 14/33] async resource --- src/glyphs.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 47a684117..b00b3d418 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -361,10 +361,10 @@ void AfterComposite(uv_work_t* req) { Nan::HandleScope scope; CompositeBaton* baton = static_cast(req->data); - + Nan::AsyncResource async_resource(__func__); if (!baton->error_name.empty()) { v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); + async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); } else { std::string& fontstack_message = *baton->message.get(); const auto argc = 2u; @@ -377,7 +377,7 @@ void AfterComposite(uv_work_t* req) { }, baton->message.release()) .ToLocalChecked()}; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); + async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); } delete baton; @@ -468,10 +468,10 @@ void AfterLoad(uv_work_t* req) { Nan::HandleScope scope; LoadBaton* baton = static_cast(req->data); - + Nan::AsyncResource async_resource(__func__); if (!baton->error_name.empty()) { v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); + async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); } else { v8::Local js_faces = Nan::New(baton->faces.size()); unsigned idx = 0; @@ -488,7 +488,7 @@ void AfterLoad(uv_work_t* req) { js_faces->Set(idx++, js_face); } v8::Local argv[2] = {Nan::Null(), js_faces}; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); + async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); } delete baton; } @@ -605,13 +605,13 @@ void AfterRange(uv_work_t* req) { Nan::HandleScope scope; RangeBaton* baton = static_cast(req->data); - + Nan::AsyncResource async_resource(__func__); if (!baton->error_name.empty()) { v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); + async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); } else { v8::Local argv[2] = {Nan::Null(), Nan::CopyBuffer(baton->message.data(), static_cast(baton->message.size())).ToLocalChecked()}; - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); + async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); } delete baton; From 8bb5316b99fef40a2ef4d6290a3db331a9c0ac57 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Thu, 28 Nov 2019 07:40:06 -0800 Subject: [PATCH 15/33] suppress spurious leak inside node --- scripts/setup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/setup.sh b/scripts/setup.sh index b5f78ebc4..99cef7326 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -85,6 +85,7 @@ function run() { echo "leak:v8::internal" >> ${SUPPRESSION_FILE} echo "leak:node::CreateEnvironment" >> ${SUPPRESSION_FILE} echo "leak:node::Init" >> ${SUPPRESSION_FILE} + echo "leak:node::Buffer::Copy" >> ${SUPPRESSION_FILE} echo "export ASAN_SYMBOLIZER_PATH=${llvm_toolchain_dir}/bin/llvm-symbolizer" >> ${config} echo "export MSAN_SYMBOLIZER_PATH=${llvm_toolchain_dir}/bin/llvm-symbolizer" >> ${config} echo "export UBSAN_OPTIONS=print_stacktrace=1" >> ${config} From c7c4112dfa60812d0b1e7e59604caf32456501a5 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Thu, 28 Nov 2019 07:40:17 -0800 Subject: [PATCH 16/33] alternative approach to fix gcc compile --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 62d8507a3..7fe321e96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -105,9 +105,9 @@ matrix: - true # g++ build (default builds all use clang++) - os: linux - # Note: -std=gnu++14 is needed to workaround: + # Note: -fext-numeric-literals is needed to workaround gcc bug: # boost/math/constants/constants.hpp:269:3: error: unable to find numeric literal operator 'operatorQ' - env: BUILDTYPE=debug CXX="g++-6" CC="gcc-6" CXXFLAGS="-std=gnu++14" + env: BUILDTYPE=debug CXX="g++-6" CC="gcc-6" CXXFLAGS="-fext-numeric-literals" node_js: 10 addons: apt: From 0ca92d2701969c87ff9e7546e4531b22ab480e43 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:00:01 -0700 Subject: [PATCH 17/33] fix addition of width, height, and left properties --- src/glyphs.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index b00b3d418..916d59ea0 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -571,8 +571,8 @@ void RangeAsync(uv_work_t* req) { // direct type conversions, no need for checking or casting glyph_writer.add_uint32(3, glyph.width); - glyph_writer.add_uint32(4, glyph.width); - glyph_writer.add_sint32(5, glyph.width); + glyph_writer.add_uint32(4, glyph.height); + glyph_writer.add_sint32(5, glyph.left); // conversions requiring checks, for safety and correctness From 38636aaae9593f9980cd2b3ebfbd7185db5dde71 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:04:14 -0700 Subject: [PATCH 18/33] ugprade deps --- package-lock.json | 139 ++++++++++++++++++++++++---------------------- package.json | 10 ++-- 2 files changed, 79 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddf952160..2e028693d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,9 +100,9 @@ } }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "code-point-at": { "version": "1.1.0", @@ -209,11 +209,11 @@ } }, "fs-minipass": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", - "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "requires": { - "minipass": "^2.2.1" + "minipass": "^2.6.0" } }, "fs.realpath": { @@ -261,9 +261,9 @@ } }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -308,9 +308,9 @@ "dev": true }, "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "requires": { "minimatch": "^3.0.4" } @@ -398,25 +398,25 @@ } }, "minimist": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", - "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "requires": { - "minipass": "^2.2.1" + "minipass": "^2.9.0" } }, "mkdirp": { @@ -440,14 +440,14 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" }, "needle": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", - "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", + "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -455,9 +455,9 @@ } }, "node-pre-gyp": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", - "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", + "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -468,30 +468,39 @@ "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^4" + "tar": "^4.4.2" } }, "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "requires": { "abbrev": "1", "osenv": "^0.1.4" } }, "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "requires": { "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" } }, "npmlog": { @@ -639,16 +648,16 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -687,9 +696,9 @@ } }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { "glob": "^7.1.3" } @@ -710,9 +719,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "set-blocking": { "version": "2.0.0", @@ -720,9 +729,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "signed-varint": { "version": "2.0.1", @@ -805,13 +814,13 @@ } }, "tar": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", - "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", + "minipass": "^2.8.6", "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", @@ -881,9 +890,9 @@ "dev": true }, "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } } diff --git a/package.json b/package.json index 16a931d02..7c9414de4 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ } ], "dependencies": { - "glob": "^7.1.1", - "minimist": "^0.2.0", - "nan": "^2.14.0", - "node-pre-gyp": "^0.12.0", - "queue-async": "^1.0.7" + "glob": "^7.1.6", + "minimist": "^1.2.5", + "nan": "^2.14.1", + "node-pre-gyp": "^0.14.0", + "queue-async": "^1.2.1" }, "devDependencies": { "@mapbox/cloudfriend": "^1.9.1", From d3339119b426e4edbaead61292dad2c041304858 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:04:55 -0700 Subject: [PATCH 19/33] no need for prepublish check --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 7c9414de4..af2981262 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,7 @@ }, "scripts": { "install": "node-pre-gyp install --fallback-to-build=true", - "test": "./node_modules/.bin/tape test/**/*.test.js", - "prepublishOnly": "npm ls" + "test": "./node_modules/.bin/tape test/**/*.test.js" }, "binary": { "module_name": "fontnik", From a46d749224f62b9630c3057efa11d69026515279 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:19:36 -0700 Subject: [PATCH 20/33] add node v14 support --- src/glyphs.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 916d59ea0..99dbf12ac 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -175,13 +175,13 @@ NAN_METHOD(Range) { } v8::Local options = info[0].As(); - v8::Local font_buffer = options->Get(Nan::New("font").ToLocalChecked()); + v8::Local font_buffer = Nan::Get(options,Nan::New("font").ToLocalChecked()).ToLocalChecked(); if (!font_buffer->IsObject()) { return Nan::ThrowTypeError("Font buffer is not an object"); } v8::Local obj = font_buffer->ToObject(Nan::GetCurrentContext()).ToLocalChecked(); - v8::Local start = options->Get(Nan::New("start").ToLocalChecked()); - v8::Local end = options->Get(Nan::New("end").ToLocalChecked()); + v8::Local start = Nan::Get(options,Nan::New("start").ToLocalChecked()).ToLocalChecked(); + v8::Local end = Nan::Get(options,Nan::New("end").ToLocalChecked()).ToLocalChecked(); if (obj->IsNull() || obj->IsUndefined() || !node::Buffer::HasInstance(obj)) { return Nan::ThrowTypeError("First argument must be a font buffer"); @@ -245,7 +245,7 @@ NAN_METHOD(Composite) { CompositeBaton* baton = new CompositeBaton(num_glyphs, callback); for (unsigned t = 0; t < num_glyphs; ++t) { - v8::Local buf_val = glyphs->Get(t); + v8::Local buf_val = Nan::Get(glyphs,t).ToLocalChecked(); if (buf_val->IsNull() || buf_val->IsUndefined()) { return utils::CallbackError("buffer value in 'glyphs' array item is null or undefined", callback); } @@ -477,15 +477,17 @@ void AfterLoad(uv_work_t* req) { unsigned idx = 0; for (auto const& face : baton->faces) { v8::Local js_face = Nan::New(); - js_face->Set(Nan::New("family_name").ToLocalChecked(), Nan::New(face.family_name).ToLocalChecked()); - if (!face.style_name.empty()) js_face->Set(Nan::New("style_name").ToLocalChecked(), Nan::New(face.style_name).ToLocalChecked()); + Nan::Set(js_face,Nan::New("family_name").ToLocalChecked(), Nan::New(face.family_name).ToLocalChecked()); + if (!face.style_name.empty()) { + Nan::Set(js_face,Nan::New("style_name").ToLocalChecked(), Nan::New(face.style_name).ToLocalChecked()); + } v8::Local js_points = Nan::New(face.points.size()); unsigned p_idx = 0; for (auto const& pt : face.points) { - js_points->Set(p_idx++, Nan::New(pt)); + Nan::Set(js_points, p_idx++, Nan::New(pt)); } - js_face->Set(Nan::New("points").ToLocalChecked(), js_points); - js_faces->Set(idx++, js_face); + Nan::Set(js_face, Nan::New("points").ToLocalChecked(), js_points); + Nan::Set(js_faces, idx++, js_face); } v8::Local argv[2] = {Nan::Null(), js_faces}; async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); From 8515f08be0297797f71dcf4ddef3c7bee6c8dc80 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:19:42 -0700 Subject: [PATCH 21/33] use latest clang++ --- scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 99cef7326..dc0ab06a3 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -4,7 +4,7 @@ set -eu set -o pipefail export MASON_RELEASE="${MASON_RELEASE:-master}" -export MASON_LLVM_RELEASE="${MASON_LLVM_RELEASE:-5.0.0}" +export MASON_LLVM_RELEASE="${MASON_LLVM_RELEASE:-10.0.0}" PLATFORM=$(uname | tr A-Z a-z) if [[ ${PLATFORM} == 'darwin' ]]; then From 0816bf72b55a97b221daedd2075c8cf4d6ebf2ea Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:23:56 -0700 Subject: [PATCH 22/33] bump to v0.6.0 --- .travis.yml | 21 +++++++++++++++++---- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7fe321e96..f12842022 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,29 +55,42 @@ matrix: - os: linux env: BUILDTYPE=debug node_js: 10 - # linux publishable node v10 + # linux publishable node v12 - os: linux env: BUILDTYPE=release node_js: 12 # linux publishable node v10/debug + - os: linux + env: BUILDTYPE=debug + node_js: 14 + # linux publishable node v14 + - os: linux + env: BUILDTYPE=release + node_js: 14 + # linux publishable node v10/debug - os: linux env: BUILDTYPE=debug node_js: 12 # osx publishable node v8 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release node_js: 8 # osx publishable node v10 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release node_js: 10 # osx publishable node v12 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release node_js: 12 + # osx publishable node v12 + - os: osx + osx_image: xcode11 + env: BUILDTYPE=release + node_js: 14 # Sanitizer build node v10/Debug - os: linux env: BUILDTYPE=debug TOOLSET=asan diff --git a/CHANGELOG.md b/CHANGELOG.md index a7bc7eb92..7eda2f14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.6.0 + +- Adds node v12 and v14 support +- Adds `fontnik.composite` +- Drops `libprotobuf` dependency, uses `protozero` instead +- Requires c++14 compatible compiler +- Binaries are published using clang++ 10.0.0 + # 0.5.2 - Adds .npmignore to keep downstream node_modules small. diff --git a/package.json b/package.json index af2981262..84aaf1ef4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fontnik", - "version": "0.5.3", + "version": "0.6.0", "description": "A library that delivers a range of glyphs rendered as SDFs (signed distance fields) in a protobuf.", "keywords": [ "font", From e8c48d2f0b7876f23896f631c12379f6e1ca86c3 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:24:33 -0700 Subject: [PATCH 23/33] make format fixes --- src/glyphs.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 99dbf12ac..d3f72b45b 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -175,13 +175,13 @@ NAN_METHOD(Range) { } v8::Local options = info[0].As(); - v8::Local font_buffer = Nan::Get(options,Nan::New("font").ToLocalChecked()).ToLocalChecked(); + v8::Local font_buffer = Nan::Get(options, Nan::New("font").ToLocalChecked()).ToLocalChecked(); if (!font_buffer->IsObject()) { return Nan::ThrowTypeError("Font buffer is not an object"); } v8::Local obj = font_buffer->ToObject(Nan::GetCurrentContext()).ToLocalChecked(); - v8::Local start = Nan::Get(options,Nan::New("start").ToLocalChecked()).ToLocalChecked(); - v8::Local end = Nan::Get(options,Nan::New("end").ToLocalChecked()).ToLocalChecked(); + v8::Local start = Nan::Get(options, Nan::New("start").ToLocalChecked()).ToLocalChecked(); + v8::Local end = Nan::Get(options, Nan::New("end").ToLocalChecked()).ToLocalChecked(); if (obj->IsNull() || obj->IsUndefined() || !node::Buffer::HasInstance(obj)) { return Nan::ThrowTypeError("First argument must be a font buffer"); @@ -245,7 +245,7 @@ NAN_METHOD(Composite) { CompositeBaton* baton = new CompositeBaton(num_glyphs, callback); for (unsigned t = 0; t < num_glyphs; ++t) { - v8::Local buf_val = Nan::Get(glyphs,t).ToLocalChecked(); + v8::Local buf_val = Nan::Get(glyphs, t).ToLocalChecked(); if (buf_val->IsNull() || buf_val->IsUndefined()) { return utils::CallbackError("buffer value in 'glyphs' array item is null or undefined", callback); } @@ -370,12 +370,13 @@ void AfterComposite(uv_work_t* req) { const auto argc = 2u; v8::Local argv[argc] = { Nan::Null(), - Nan::NewBuffer(&fontstack_message[0], - static_cast(fontstack_message.size()), - [](char*, void* hint) { - delete reinterpret_cast(hint); - }, - baton->message.release()) + Nan::NewBuffer( + &fontstack_message[0], + static_cast(fontstack_message.size()), + [](char*, void* hint) { + delete reinterpret_cast(hint); + }, + baton->message.release()) .ToLocalChecked()}; async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 2, argv); } @@ -477,9 +478,9 @@ void AfterLoad(uv_work_t* req) { unsigned idx = 0; for (auto const& face : baton->faces) { v8::Local js_face = Nan::New(); - Nan::Set(js_face,Nan::New("family_name").ToLocalChecked(), Nan::New(face.family_name).ToLocalChecked()); + Nan::Set(js_face, Nan::New("family_name").ToLocalChecked(), Nan::New(face.family_name).ToLocalChecked()); if (!face.style_name.empty()) { - Nan::Set(js_face,Nan::New("style_name").ToLocalChecked(), Nan::New(face.style_name).ToLocalChecked()); + Nan::Set(js_face, Nan::New("style_name").ToLocalChecked(), Nan::New(face.style_name).ToLocalChecked()); } v8::Local js_points = Nan::New(face.points.size()); unsigned p_idx = 0; From d87eb16d7aff6377b6fc105cc91d1bced62d5a97 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:27:14 -0700 Subject: [PATCH 24/33] avoid header guard check --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 3e5ef7d22..2c3e45195 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '*' +Checks: '*,-llvm-header-guard' WarningsAsErrors: '*' HeaderFilterRegex: '\/src\/' AnalyzeTemporaryDtors: false From ab24bd81c9d79abef9515c8a189b3c13d0a8a34b Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 16:33:29 -0700 Subject: [PATCH 25/33] bump to 0.6.0-dev.1, update readme [publish binary] --- README.md | 8 +++++--- package.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 38d88e724..4f307f1e4 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ [![Build Status](https://travis-ci.org/mapbox/node-fontnik.svg?branch=master)](https://travis-ci.org/mapbox/node-fontnik) [![codecov](https://codecov.io/gh/mapbox/node-fontnik/branch/master/graph/badge.svg)](https://codecov.io/gh/mapbox/node-fontnik) -A library that delivers a range of glyphs rendered as SDFs (signed distance fields) in a protocol buffer. We use these encoded glyphs as the basic blocks of font rendering in [Mapbox GL](https://github.com/mapbox/mapbox-gl-js). SDF encoding is superior to traditional fonts for our usecase terms of scaling, rotation, and quickly deriving halos - WebGL doesn't have built-in font rendering, so the decision is between vectorization, which tends to be slow, and SDF generation. +A library that delivers a range of glyphs rendered as SDFs (signed distance fields) in a protocol buffer. We use these encoded glyphs as the basic blocks of font rendering in [Mapbox GL](https://github.com/mapbox/mapbox-gl-js). SDF encoding is superior to traditional fonts for our usecase in terms of scaling, rotation, and quickly deriving halos - WebGL doesn't have built-in font rendering, so the decision is between vectorization, which tends to be slow, and SDF generation. The approach this library takes is to parse and rasterize the font with Freetype (hence the C++ requirement), and then generate a distance field from that rasterized image. +See also [TinySDF](https://github.com/mapbox/tiny-sdf), which is a faster but less precise approach to generating SDFs for fonts. + ## [API](API.md) ## Installing @@ -15,7 +17,7 @@ The approach this library takes is to parse and rasterize the font with Freetype By default, installs binaries. On these platforms no external dependencies are needed. - 64 bit OS X or 64 bit Linux -- Node.js v0.10.x, v0.12.x, v4.x or v6.x +- Node.js v8-v14 Just run: @@ -30,7 +32,7 @@ However, other platforms will fall back to a source compile: see [building from ``` npm install --build-from-source ``` -Building from source should automatically install `boost`, `freetype` and `protobuf` locally using [mason](https://github.com/mapbox/mason). These dependencies can be installed manually by running `./scripts/install_deps.sh`. +Building from source should automatically install `boost`, `freetype` and `protozero` locally using [mason](https://github.com/mapbox/mason). These dependencies can be installed manually by running `./scripts/install_deps.sh`. ## Background reading - [Drawing Text with Signed Distance Fields in Mapbox GL](https://www.mapbox.com/blog/text-signed-distance-fields/) diff --git a/package.json b/package.json index 84aaf1ef4..4542131e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fontnik", - "version": "0.6.0", + "version": "0.6.0-dev.1", "description": "A library that delivers a range of glyphs rendered as SDFs (signed distance fields) in a protobuf.", "keywords": [ "font", From f80b4cc1d9844e9d61c89ad9013ae2937a36aa43 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 17:28:57 -0700 Subject: [PATCH 26/33] clang tidy fixes --- src/glyphs.cpp | 112 ++++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index d3f72b45b..7f478dcb7 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -14,6 +14,8 @@ #include #include #include +#include + namespace node_fontnik { @@ -27,13 +29,13 @@ struct FaceMetadata { std::string family_name{}; std::string style_name{}; std::vector points{}; - FaceMetadata(std::string const& _family_name, - std::string const& _style_name, - std::vector&& _points) : family_name(_family_name), - style_name(_style_name), + FaceMetadata(std::string _family_name, + std::string _style_name, + std::vector&& _points) : family_name(std::move(_family_name)), + style_name(std::move(_style_name)), points(std::move(_points)) {} - FaceMetadata(std::string const& _family_name, - std::vector&& _points) : family_name(_family_name), + FaceMetadata(std::string _family_name, + std::vector&& _points) : family_name(std::move(_family_name)), points(std::move(_points)) {} }; @@ -55,8 +57,7 @@ struct LoadBaton { LoadBaton(v8::Local buf, v8::Local cb) : font_data(node::Buffer::Data(buf)), font_size(node::Buffer::Length(buf)), - error_name(), - faces(), + request() { request.data = this; callback.Reset(cb.As()); @@ -90,11 +91,10 @@ struct RangeBaton { std::uint32_t _start, std::uint32_t _end) : font_data(node::Buffer::Data(buf)), font_size(node::Buffer::Length(buf)), - error_name(), + start(_start), end(_end), - chars(), - message(), + request() { request.data = this; callback.Reset(cb.As()); @@ -107,9 +107,9 @@ struct RangeBaton { }; struct GlyphPBF { - GlyphPBF(v8::Local& buffer) + explicit GlyphPBF(v8::Local& buffer) : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)}, - buffer_ref{} { + { buffer_ref.Reset(buffer.As()); } @@ -138,7 +138,7 @@ struct CompositeBaton { std::string error_name; std::unique_ptr message; uv_work_t request; - CompositeBaton(unsigned size, v8::Local cb) : error_name(), + CompositeBaton(unsigned size, v8::Local cb) : message(std::make_unique()), request() { glyphs.reserve(size); @@ -164,7 +164,7 @@ NAN_METHOD(Load) { return Nan::ThrowTypeError("Callback must be a function"); } - LoadBaton* baton = new LoadBaton(obj, info[1]); + auto* baton = new LoadBaton(obj, info[1]); uv_queue_work(uv_default_loop(), &baton->request, LoadAsync, reinterpret_cast(AfterLoad)); } @@ -203,7 +203,7 @@ NAN_METHOD(Range) { return Nan::ThrowTypeError("Callback must be a function"); } - RangeBaton* baton = new RangeBaton(obj, + auto* baton = new RangeBaton(obj, info[1], Nan::To(start).FromJust(), Nan::To(end).FromJust()); @@ -212,7 +212,7 @@ NAN_METHOD(Range) { namespace utils { -inline void CallbackError(std::string message, v8::Local func) { +inline void CallbackError(const std::string& message, v8::Local func) { Nan::Callback cb(func); v8::Local argv[1] = {Nan::Error(message.c_str())}; Nan::Call(cb, 1, argv); @@ -242,7 +242,7 @@ NAN_METHOD(Composite) { return utils::CallbackError("'glyphs' array must be of length greater than 0", callback); } - CompositeBaton* baton = new CompositeBaton(num_glyphs, callback); + auto* baton = new CompositeBaton(num_glyphs, callback); for (unsigned t = 0; t < num_glyphs; ++t) { v8::Local buf_val = Nan::Get(glyphs, t).ToLocalChecked(); @@ -266,19 +266,19 @@ NAN_METHOD(Composite) { using id_pair = std::pair; struct CompareID { bool operator()(id_pair const& r1, id_pair const& r2) { - return r1.first - r2.first; + return (r1.first - r2.first) != 0u; } }; void CompositeAsync(uv_work_t* req) { - CompositeBaton* baton = static_cast(req->data); + auto* baton = static_cast(req->data); try { std::vector>> buffer_cache; std::map id_mapping; bool first_buffer = true; std::string fontstack_name; std::string range; - std::string& fontstack_buffer = *baton->message.get(); + std::string& fontstack_buffer = *baton->message; protozero::pbf_writer pbf_writer(fontstack_buffer); protozero::pbf_writer fontstack_writer{pbf_writer, 1}; // TODO(danespringmeyer): avoid duplicate fontstacks to be sent it @@ -360,20 +360,20 @@ void CompositeAsync(uv_work_t* req) { void AfterComposite(uv_work_t* req) { Nan::HandleScope scope; - CompositeBaton* baton = static_cast(req->data); + auto* baton = static_cast(req->data); Nan::AsyncResource async_resource(__func__); if (!baton->error_name.empty()) { v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; async_resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), Nan::New(baton->callback), 1, argv); } else { - std::string& fontstack_message = *baton->message.get(); - const auto argc = 2u; + std::string& fontstack_message = *baton->message; + const auto argc = 2U; v8::Local argv[argc] = { Nan::Null(), Nan::NewBuffer( &fontstack_message[0], static_cast(fontstack_message.size()), - [](char*, void* hint) { + [](char* /*unused*/, void* hint) { delete reinterpret_cast(hint); }, baton->message.release()) @@ -389,10 +389,12 @@ struct ft_library_guard { ft_library_guard(ft_library_guard const&) = delete; ft_library_guard& operator=(ft_library_guard const&) = delete; - ft_library_guard(FT_Library* lib) : library_(lib) {} + explicit ft_library_guard(FT_Library* lib) : library_(lib) {} ~ft_library_guard() { - if (library_) FT_Done_FreeType(*library_); + if (library_ != nullptr) { FT_Done_FreeType(*library_); + +} } FT_Library* library_; @@ -402,10 +404,10 @@ struct ft_face_guard { // non copyable ft_face_guard(ft_face_guard const&) = delete; ft_face_guard& operator=(ft_face_guard const&) = delete; - ft_face_guard(FT_Face* f) : face_(f) {} + explicit ft_face_guard(FT_Face* f) : face_(f) {} ~ft_face_guard() { - if (face_) { + if (face_ != nullptr) { FT_Done_Face(*face_); } } @@ -414,23 +416,23 @@ struct ft_face_guard { }; void LoadAsync(uv_work_t* req) { - LoadBaton* baton = static_cast(req->data); + auto* baton = static_cast(req->data); try { FT_Library library = nullptr; ft_library_guard library_guard(&library); FT_Error error = FT_Init_FreeType(&library); - if (error) { + if (error != 0) { /* LCOV_EXCL_START */ baton->error_name = std::string("could not open FreeType library"); return; /* LCOV_EXCL_END */ } - FT_Face ft_face = 0; + FT_Face ft_face = nullptr; FT_Long num_faces = 0; - for (int i = 0; ft_face == 0 || i < num_faces; ++i) { + for (int i = 0; ft_face == nullptr || i < num_faces; ++i) { ft_face_guard face_guard(&ft_face); FT_Error face_error = FT_New_Memory_Face(library, reinterpret_cast(baton->font_data), static_cast(baton->font_size), i, &ft_face); - if (face_error) { + if (face_error != 0) { baton->error_name = std::string("could not open font file"); return; } @@ -438,19 +440,21 @@ void LoadAsync(uv_work_t* req) { num_faces = ft_face->num_faces; } - if (ft_face->family_name) { + if (ft_face->family_name != nullptr) { std::set points; FT_ULong charcode; FT_UInt gindex; charcode = FT_Get_First_Char(ft_face, &gindex); while (gindex != 0) { charcode = FT_Get_Next_Char(ft_face, charcode, &gindex); - if (charcode != 0) points.emplace(charcode); + if (charcode != 0) { points.emplace(charcode); + +} } std::vector points_vec(points.begin(), points.end()); - if (ft_face->style_name) { + if (ft_face->style_name != nullptr) { baton->faces.emplace_back(ft_face->family_name, ft_face->style_name, std::move(points_vec)); } else { baton->faces.emplace_back(ft_face->family_name, std::move(points_vec)); @@ -468,7 +472,7 @@ void LoadAsync(uv_work_t* req) { void AfterLoad(uv_work_t* req) { Nan::HandleScope scope; - LoadBaton* baton = static_cast(req->data); + auto* baton = static_cast(req->data); Nan::AsyncResource async_resource(__func__); if (!baton->error_name.empty()) { v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; @@ -497,7 +501,7 @@ void AfterLoad(uv_work_t* req) { } void RangeAsync(uv_work_t* req) { - RangeBaton* baton = static_cast(req->data); + auto* baton = static_cast(req->data); try { unsigned array_size = baton->end - baton->start; @@ -509,7 +513,7 @@ void RangeAsync(uv_work_t* req) { FT_Library library = nullptr; ft_library_guard library_guard(&library); FT_Error error = FT_Init_FreeType(&library); - if (error) { + if (error != 0) { /* LCOV_EXCL_START */ baton->error_name = std::string("could not open FreeType library"); return; @@ -517,12 +521,12 @@ void RangeAsync(uv_work_t* req) { } protozero::pbf_writer pbf_writer{baton->message}; - FT_Face ft_face = 0; + FT_Face ft_face = nullptr; FT_Long num_faces = 0; - for (int i = 0; ft_face == 0 || i < num_faces; ++i) { + for (int i = 0; ft_face == nullptr || i < num_faces; ++i) { ft_face_guard face_guard(&ft_face); FT_Error face_error = FT_New_Memory_Face(library, reinterpret_cast(baton->font_data), static_cast(baton->font_size), i, &ft_face); - if (face_error) { + if (face_error != 0) { baton->error_name = std::string("could not open font"); return; } @@ -531,9 +535,9 @@ void RangeAsync(uv_work_t* req) { num_faces = ft_face->num_faces; } - if (ft_face->family_name) { + if (ft_face->family_name != nullptr) { protozero::pbf_writer fontstack_writer{pbf_writer, 1}; - if (ft_face->style_name) { + if (ft_face->style_name != nullptr) { fontstack_writer.add_string(1, std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); } else { fontstack_writer.add_string(1, std::string(ft_face->family_name)); @@ -553,7 +557,9 @@ void RangeAsync(uv_work_t* req) { // Get FreeType face from face_ptr. FT_UInt char_index = FT_Get_Char_Index(ft_face, char_code); - if (!char_index) continue; + if (char_index == 0u) { continue; + +} glyph.glyph_index = char_index; sdf_glyph_foundry::RenderSDF(glyph, 24, 3, 0.25, ft_face); @@ -564,9 +570,9 @@ void RangeAsync(uv_work_t* req) { // shortening conversion if (char_code > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for char_code: too large"); - } else { + } glyph_writer.add_uint32(1, static_cast(char_code)); - } + if (glyph.width > 0) { glyph_writer.add_bytes(2, glyph.bitmap); @@ -583,16 +589,16 @@ void RangeAsync(uv_work_t* req) { double top = static_cast(glyph.top) - glyph.ascender; if (top < std::numeric_limits::min() || top > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); - } else { + } glyph_writer.add_sint32(6, static_cast(top)); - } + // double to uint if (glyph.advance < std::numeric_limits::min() || glyph.advance > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); - } else { + } glyph_writer.add_uint32(7, static_cast(glyph.advance)); - } + } } else { baton->error_name = std::string("font does not have family_name"); @@ -607,7 +613,7 @@ void RangeAsync(uv_work_t* req) { void AfterRange(uv_work_t* req) { Nan::HandleScope scope; - RangeBaton* baton = static_cast(req->data); + auto* baton = static_cast(req->data); Nan::AsyncResource async_resource(__func__); if (!baton->error_name.empty()) { v8::Local argv[1] = {Nan::Error(baton->error_name.c_str())}; From 1c14486c32163317e00e4935f1074d51a2445ebe Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 17:29:16 -0700 Subject: [PATCH 27/33] fewer clang-tidy checks --- .clang-tidy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 2c3e45195..44c232a7b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '*,-llvm-header-guard' +Checks: '*,-llvm-header-guard,-fuchsia*,-modernize-use-trailing-return-type,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers' WarningsAsErrors: '*' HeaderFilterRegex: '\/src\/' AnalyzeTemporaryDtors: false From bc0db8c8f323d23f7806acff37b365306a48aefb Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 17:30:35 -0700 Subject: [PATCH 28/33] more clang tidy fixes --- src/glyphs.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 7f478dcb7..394c744e4 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -266,7 +266,7 @@ NAN_METHOD(Composite) { using id_pair = std::pair; struct CompareID { bool operator()(id_pair const& r1, id_pair const& r2) { - return (r1.first - r2.first) != 0u; + return (r1.first - r2.first) != 0U; } }; @@ -557,7 +557,7 @@ void RangeAsync(uv_work_t* req) { // Get FreeType face from face_ptr. FT_UInt char_index = FT_Get_Char_Index(ft_face, char_code); - if (char_index == 0u) { continue; + if (char_index == 0U) { continue; } From 77362281b98fdd9c42f955cc4fef699b645c0642 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Fri, 22 May 2020 18:35:21 -0700 Subject: [PATCH 29/33] disable clang-tidy for the moment --- .travis.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index f12842022..0f371fc55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -164,15 +164,15 @@ matrix: # Overrides `script`, no need to run tests before_script: # Clang tidy build - - os: linux - env: CLANG_TIDY - node_js: 10 - # Overrides `install` to avoid initializing clang toolchain - install: - # First run the clang-tidy target - # Any code formatting fixes automatically applied by clang-tidy - # will trigger the build to fail (idea here is to get us to pay attention - # and get in the habit of running these locally before committing) - - make tidy - # Overrides `script`, no need to run tests - before_script: + # - os: linux + # env: CLANG_TIDY + # node_js: 10 + # # Overrides `install` to avoid initializing clang toolchain + # install: + # # First run the clang-tidy target + # # Any code formatting fixes automatically applied by clang-tidy + # # will trigger the build to fail (idea here is to get us to pay attention + # # and get in the habit of running these locally before committing) + # - make tidy + # # Overrides `script`, no need to run tests + # before_script: From 81d07d86414d42b734c6c490158d5a98fecc11e7 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Sat, 23 May 2020 08:36:05 -0700 Subject: [PATCH 30/33] add to npm ignore --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index e44091ef4..d49c91b31 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,7 @@ .mason .toolchain bench +glyphs build fonts lib From 30d2200fc888b4577731f21d8647641e2364052d Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 25 May 2020 18:23:52 -0700 Subject: [PATCH 31/33] fix merge problem --- src/glyphs.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 394c744e4..582db69b8 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -108,10 +108,9 @@ struct RangeBaton { struct GlyphPBF { explicit GlyphPBF(v8::Local& buffer) - : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)}, - { - buffer_ref.Reset(buffer.As()); - } + : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)} { + buffer_ref.Reset(buffer.As()); + } ~GlyphPBF() { buffer_ref.Reset(); From 5bc8dfe7fffec3c16dc715641e204f2481f61df5 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 25 May 2020 18:25:02 -0700 Subject: [PATCH 32/33] make format --- src/glyphs.cpp | 59 +++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 582db69b8..fa7ea832b 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -16,7 +16,6 @@ #include #include - namespace node_fontnik { struct FaceMetadata { @@ -29,12 +28,12 @@ struct FaceMetadata { std::string family_name{}; std::string style_name{}; std::vector points{}; - FaceMetadata(std::string _family_name, - std::string _style_name, + FaceMetadata(std::string _family_name, + std::string _style_name, std::vector&& _points) : family_name(std::move(_family_name)), style_name(std::move(_style_name)), points(std::move(_points)) {} - FaceMetadata(std::string _family_name, + FaceMetadata(std::string _family_name, std::vector&& _points) : family_name(std::move(_family_name)), points(std::move(_points)) {} }; @@ -57,7 +56,7 @@ struct LoadBaton { LoadBaton(v8::Local buf, v8::Local cb) : font_data(node::Buffer::Data(buf)), font_size(node::Buffer::Length(buf)), - + request() { request.data = this; callback.Reset(cb.As()); @@ -91,10 +90,10 @@ struct RangeBaton { std::uint32_t _start, std::uint32_t _end) : font_data(node::Buffer::Data(buf)), font_size(node::Buffer::Length(buf)), - + start(_start), end(_end), - + request() { request.data = this; callback.Reset(cb.As()); @@ -109,8 +108,8 @@ struct RangeBaton { struct GlyphPBF { explicit GlyphPBF(v8::Local& buffer) : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)} { - buffer_ref.Reset(buffer.As()); - } + buffer_ref.Reset(buffer.As()); + } ~GlyphPBF() { buffer_ref.Reset(); @@ -137,8 +136,7 @@ struct CompositeBaton { std::string error_name; std::unique_ptr message; uv_work_t request; - CompositeBaton(unsigned size, v8::Local cb) : - message(std::make_unique()), + CompositeBaton(unsigned size, v8::Local cb) : message(std::make_unique()), request() { glyphs.reserve(size); request.data = this; @@ -203,9 +201,9 @@ NAN_METHOD(Range) { } auto* baton = new RangeBaton(obj, - info[1], - Nan::To(start).FromJust(), - Nan::To(end).FromJust()); + info[1], + Nan::To(start).FromJust(), + Nan::To(end).FromJust()); uv_queue_work(uv_default_loop(), &baton->request, RangeAsync, reinterpret_cast(AfterRange)); } @@ -391,9 +389,9 @@ struct ft_library_guard { explicit ft_library_guard(FT_Library* lib) : library_(lib) {} ~ft_library_guard() { - if (library_ != nullptr) { FT_Done_FreeType(*library_); - -} + if (library_ != nullptr) { + FT_Done_FreeType(*library_); + } } FT_Library* library_; @@ -446,9 +444,9 @@ void LoadAsync(uv_work_t* req) { charcode = FT_Get_First_Char(ft_face, &gindex); while (gindex != 0) { charcode = FT_Get_Next_Char(ft_face, charcode, &gindex); - if (charcode != 0) { points.emplace(charcode); - -} + if (charcode != 0) { + points.emplace(charcode); + } } std::vector points_vec(points.begin(), points.end()); @@ -556,9 +554,9 @@ void RangeAsync(uv_work_t* req) { // Get FreeType face from face_ptr. FT_UInt char_index = FT_Get_Char_Index(ft_face, char_code); - if (char_index == 0U) { continue; - -} + if (char_index == 0U) { + continue; + } glyph.glyph_index = char_index; sdf_glyph_foundry::RenderSDF(glyph, 24, 3, 0.25, ft_face); @@ -569,9 +567,8 @@ void RangeAsync(uv_work_t* req) { // shortening conversion if (char_code > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for char_code: too large"); - } - glyph_writer.add_uint32(1, static_cast(char_code)); - + } + glyph_writer.add_uint32(1, static_cast(char_code)); if (glyph.width > 0) { glyph_writer.add_bytes(2, glyph.bitmap); @@ -588,16 +585,14 @@ void RangeAsync(uv_work_t* req) { double top = static_cast(glyph.top) - glyph.ascender; if (top < std::numeric_limits::min() || top > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); - } - glyph_writer.add_sint32(6, static_cast(top)); - + } + glyph_writer.add_sint32(6, static_cast(top)); // double to uint if (glyph.advance < std::numeric_limits::min() || glyph.advance > std::numeric_limits::max()) { throw std::runtime_error("Invalid value for glyph.top-glyph.ascender"); - } - glyph_writer.add_uint32(7, static_cast(glyph.advance)); - + } + glyph_writer.add_uint32(7, static_cast(glyph.advance)); } } else { baton->error_name = std::string("font does not have family_name"); From 1d0c8957bc95d04cde052efc912cc02a7964365e Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Mon, 25 May 2020 18:51:52 -0700 Subject: [PATCH 33/33] bump to v0.6.0 [publish binary] --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eda2f14f..cddffa4de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.6.0 - Adds node v12 and v14 support +- Dropped node v4 and v6 support - Adds `fontnik.composite` - Drops `libprotobuf` dependency, uses `protozero` instead - Requires c++14 compatible compiler diff --git a/package.json b/package.json index 4542131e4..84aaf1ef4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fontnik", - "version": "0.6.0-dev.1", + "version": "0.6.0", "description": "A library that delivers a range of glyphs rendered as SDFs (signed distance fields) in a protobuf.", "keywords": [ "font",