diff --git a/.clang-tidy b/.clang-tidy index 3e5ef7d22..44c232a7b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- -Checks: '*' +Checks: '*,-llvm-header-guard,-fuchsia*,-modernize-use-trailing-return-type,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers' WarningsAsErrors: '*' HeaderFilterRegex: '\/src\/' AnalyzeTemporaryDtors: false 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 diff --git a/.travis.yml b/.travis.yml index 2830a00e8..0f371fc55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,62 +39,62 @@ script: # run your tests and build binaries matrix: include: - # linux publishable node v4/release + # linux publishable node v8 - os: linux env: BUILDTYPE=release - node_js: 4 - # linux publishable node v4/debug + node_js: 8 + # linux publishable node v8/debug - os: linux env: BUILDTYPE=debug - node_js: 4 - # linux publishable node v6 + node_js: 8 + # linux publishable node v10 - os: linux env: BUILDTYPE=release - node_js: 6 - # linux publishable node v6/debug + node_js: 10 + # linux publishable node v10/debug - os: linux env: BUILDTYPE=debug - node_js: 6 - # linux publishable node v8 + node_js: 10 + # linux publishable node v12 - os: linux env: BUILDTYPE=release - node_js: 8 - # linux publishable node v8/debug + node_js: 12 + # linux publishable node v10/debug - os: linux env: BUILDTYPE=debug - node_js: 8 - # linux publishable node v8 + node_js: 14 + # linux publishable node v14 - os: linux env: BUILDTYPE=release - node_js: 10 - # linux publishable node v8/debug + node_js: 14 + # linux publishable node v10/debug - os: linux env: BUILDTYPE=debug - node_js: 10 - # osx publishable node v4 + node_js: 12 + # osx publishable node v8 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release - node_js: 4 - # osx publishable node v6 + node_js: 8 + # osx publishable node v10 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release - node_js: 6 - # osx publishable node v6 + node_js: 10 + # osx publishable node v12 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release - node_js: 8 - # osx publishable node v6 + node_js: 12 + # osx publishable node v12 - os: osx - osx_image: xcode8.2 + osx_image: xcode11 env: BUILDTYPE=release - node_js: 10 - # Sanitizer build node v4/Debug + node_js: 14 + # 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: @@ -118,8 +118,10 @@ matrix: - true # g++ build (default builds all use clang++) - os: linux - env: BUILDTYPE=debug CXX="g++-6" CC="gcc-6" - node_js: 4 + # 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="-fext-numeric-literals" + node_js: 10 addons: apt: sources: @@ -136,7 +138,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 @@ -162,15 +164,15 @@ matrix: # Overrides `script`, no need to run tests before_script: # Clang tidy build - - os: linux - env: CLANG_TIDY - node_js: 4 - # 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: diff --git a/CHANGELOG.md b/CHANGELOG.md index a7bc7eb92..cddffa4de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 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 +- Binaries are published using clang++ 10.0.0 + # 0.5.2 - Adds .npmignore to keep downstream node_modules small. diff --git a/README.md b/README.md index ac98816de..1c8b5c380 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`. ## Local testing diff --git a/binding.gyp b/binding.gyp index 3acdaa0fe..364620afa 100644 --- a/binding.gyp +++ b/binding.gyp @@ -9,8 +9,7 @@ 'system_includes': [ "-isystem <(module_root_dir)/> ${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} diff --git a/src/glyphs.cpp b/src/glyphs.cpp index 9037aec44..fa7ea832b 100644 --- a/src/glyphs.cpp +++ b/src/glyphs.cpp @@ -1,14 +1,20 @@ // fontnik #include "glyphs.hpp" - +#include +#include // node #include +#include #include #include // sdf-glyph-foundry +#include +#include +#include #include #include +#include namespace node_fontnik { @@ -22,13 +28,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)) {} }; @@ -50,8 +56,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()); @@ -85,11 +90,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()); @@ -101,12 +105,54 @@ struct RangeBaton { } }; +struct GlyphPBF { + explicit GlyphPBF(v8::Local& buffer) + : data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)} { + 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) : 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()) { 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"); } @@ -115,7 +161,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)); } @@ -126,27 +172,27 @@ 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(); - v8::Local start = options->Get(Nan::New("start").ToLocalChecked()); - v8::Local end = options->Get(Nan::New("end").ToLocalChecked()); + 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(); if (obj->IsNull() || obj->IsUndefined() || !node::Buffer::HasInstance(obj)) { 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`"); } @@ -154,22 +200,198 @@ NAN_METHOD(Range) { return Nan::ThrowTypeError("Callback must be a function"); } - RangeBaton* baton = new RangeBaton(obj, - info[1], - start->Uint32Value(), - end->Uint32Value()); + auto* baton = new RangeBaton(obj, + info[1], + Nan::To(start).FromJust(), + Nan::To(end).FromJust()); uv_queue_work(uv_default_loop(), &baton->request, RangeAsync, reinterpret_cast(AfterRange)); } +namespace utils { + +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); +} + +} // namespace utils + +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); + } + + auto* baton = new CompositeBaton(num_glyphs, callback); + + for (unsigned t = 0; t < num_glyphs; ++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); + } + 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); + } + 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) != 0U; + } +}; + +void CompositeAsync(uv_work_t* req) { + 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; + 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; + + 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; + const auto argc = 2U; + v8::Local argv[argc] = { + Nan::Null(), + Nan::NewBuffer( + &fontstack_message[0], + static_cast(fontstack_message.size()), + [](char* /*unused*/, void* hint) { + delete reinterpret_cast(hint); + }, + baton->message.release()) + .ToLocalChecked()}; + async_resource.runInAsyncScope(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; 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_; @@ -179,10 +401,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_); } } @@ -191,23 +413,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; } @@ -215,19 +437,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)); @@ -245,34 +469,36 @@ 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())}; - 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; 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}; - 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; } 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; @@ -284,20 +510,20 @@ 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; /* LCOV_EXCL_END */ } - llmr::glyphs::glyphs glyphs; - FT_Face ft_face = 0; + protozero::pbf_writer pbf_writer{baton->message}; + 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; } @@ -306,15 +532,14 @@ void RangeAsync(uv_work_t* req) { num_faces = ft_face->num_faces; } - if (ft_face->family_name) { - llmr::glyphs::fontstack* mutable_fontstack = glyphs.add_stacks(); - if (ft_face->style_name) { - mutable_fontstack->set_name(std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); + if (ft_face->family_name != nullptr) { + protozero::pbf_writer fontstack_writer{pbf_writer, 1}; + if (ft_face->style_name != nullptr) { + 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; @@ -329,53 +554,51 @@ 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); // 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.height); + glyph_writer.add_sint32(5, glyph.left); + + // 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)); - } - - if (glyph.width > 0) { - mutable_glyph->set_bitmap(glyph.bitmap); } + glyph_writer.add_uint32(7, static_cast(glyph.advance)); } } 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(); } @@ -384,14 +607,14 @@ 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())}; - 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; diff --git a/src/glyphs.hpp b/src/glyphs.hpp index ecfc8cb94..ced817a83 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 { @@ -12,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..c7c31248c 100644 --- a/src/node_fontnik.cpp +++ b/src/node_fontnik.cpp @@ -4,14 +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()); +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 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 000000000..a7d345002 Binary files /dev/null and b/test/fixtures/arialunicode.512.767.pbf differ diff --git a/test/fixtures/league.512.767.pbf b/test/fixtures/league.512.767.pbf new file mode 100644 index 000000000..eb64b5620 Binary files /dev/null and b/test/fixtures/league.512.767.pbf differ 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 000000000..72bb299dc Binary files /dev/null and b/test/fixtures/league.opensans.arialunicode.512.767.pbf differ diff --git a/test/fixtures/opensans.512.767.pbf b/test/fixtures/opensans.512.767.pbf new file mode 100644 index 000000000..5dbfb5b43 Binary files /dev/null and b/test/fixtures/opensans.512.767.pbf differ diff --git a/test/fixtures/opensans.arialunicode.512.767.pbf b/test/fixtures/opensans.arialunicode.512.767.pbf new file mode 100644 index 000000000..58429df2b Binary files /dev/null and b/test/fixtures/opensans.arialunicode.512.767.pbf differ