├── fonts ├── osaka │ └── Osaka.ttf ├── open-sans │ ├── OpenSans-Regular.ttf │ └── Apache License.txt ├── firasans-medium │ ├── FiraSans-Medium.ttf │ └── LICENSE └── GuardianTextSansWeb │ ├── GuardianTextSansWeb-Bold.ttf │ └── LICENSE.md ├── index.js ├── .gitmodules ├── test ├── fixtures │ ├── range.0.256.pbf │ ├── league.512.767.pbf │ ├── opensans.512.767.pbf │ ├── arialunicode.512.767.pbf │ ├── fonts │ │ ├── FiraSans-Medium.ttf │ │ └── OpenSans-Regular.ttf │ ├── opensans.arialunicode.512.767.pbf │ ├── league.opensans.arialunicode.512.767.pbf │ └── fonts-invalid │ │ ├── 1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf │ │ └── README.md ├── composite.test.js ├── format │ └── glyphs.js ├── expected │ ├── load.json │ └── range.json ├── bin.test.js └── fontnik.test.js ├── .gitignore ├── .npmignore ├── bin ├── namespace ├── build-glyphs └── font-inspect ├── src ├── glyphs.hpp ├── node_fontnik.cpp └── glyphs.cpp ├── scripts ├── install_deps.sh ├── clang-format.sh ├── coverage.sh ├── sanitize.sh ├── create_scheme.sh ├── generate_compile_commands.py ├── clang-tidy.sh ├── library.xcscheme ├── node.xcscheme └── setup.sh ├── proto └── glyphs.proto ├── bench ├── bench2.js └── bench.js ├── API.md ├── common.gypi ├── .clang-tidy ├── DEV.md ├── LICENSE.txt ├── CODE_OF_CONDUCT.md ├── package.json ├── Makefile ├── README.md ├── CHANGELOG.md ├── .clang-format ├── .github └── workflows │ └── builds.yml └── binding.gyp /fonts/osaka/Osaka.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/fonts/osaka/Osaka.ttf -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var binding = require('node-gyp-build')(__dirname) 2 | 3 | module.exports = binding 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".mason"] 2 | path = .mason 3 | url = https://github.com/mapbox/mason.git 4 | -------------------------------------------------------------------------------- /test/fixtures/range.0.256.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/range.0.256.pbf -------------------------------------------------------------------------------- /test/fixtures/league.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/league.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/opensans.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/opensans.512.767.pbf -------------------------------------------------------------------------------- /fonts/open-sans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/fonts/open-sans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /test/fixtures/arialunicode.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/arialunicode.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/fonts/FiraSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/fonts/FiraSans-Medium.ttf -------------------------------------------------------------------------------- /fonts/firasans-medium/FiraSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/fonts/firasans-medium/FiraSans-Medium.ttf -------------------------------------------------------------------------------- /test/fixtures/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /test/fixtures/opensans.arialunicode.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/opensans.arialunicode.512.767.pbf -------------------------------------------------------------------------------- /fonts/GuardianTextSansWeb/GuardianTextSansWeb-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/fonts/GuardianTextSansWeb/GuardianTextSansWeb-Bold.ttf -------------------------------------------------------------------------------- /test/fixtures/league.opensans.arialunicode.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/league.opensans.arialunicode.512.767.pbf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | lib 3 | glyphs 4 | spec 5 | node_modules 6 | mason_packages 7 | .DS_Store 8 | lib/binding 9 | .toolchain 10 | .mason 11 | local.env 12 | prebuilds 13 | *.tgz -------------------------------------------------------------------------------- /test/fixtures/fonts-invalid/1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/HEAD/test/fixtures/fonts-invalid/1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .clang* 2 | .mason 3 | .toolchain 4 | bench 5 | glyphs 6 | build 7 | fonts 8 | lib 9 | mason_packages 10 | node_modules 11 | test 12 | LICENSE.txt 13 | cloudformation 14 | .travis.yml 15 | .gitmodules 16 | local.env 17 | *.tgz 18 | DEV.md 19 | CODE_OF_CONDUCT.md 20 | API.md 21 | -------------------------------------------------------------------------------- /bin/namespace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | find ./ -type f -name '*.cpp' -or -name '*.h' -or -name '*.hpp' | xargs perl -i -p -e "s/namespace $1(?!_fontnik)/namespace $1_fontnik/g;" 4 | find ./ -type f -name '*.cpp' -or -name '*.h' -or -name '*.hpp' | xargs perl -i -p -e "s/$1::/$1_fontnik::/g;" 5 | -------------------------------------------------------------------------------- /src/glyphs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace node_fontnik { 7 | 8 | Napi::Value Load(Napi::CallbackInfo const& info); 9 | Napi::Value Range(Napi::CallbackInfo const& info); 10 | Napi::Value Composite(Napi::CallbackInfo const& info); 11 | 12 | } // namespace node_fontnik 13 | -------------------------------------------------------------------------------- /scripts/install_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | function install() { 7 | mason install $1 $2 8 | mason link $1 $2 9 | } 10 | 11 | # setup mason 12 | ./scripts/setup.sh --config local.env 13 | source local.env 14 | 15 | install boost 1.67.0 16 | install freetype 2.7.1 17 | install protozero 1.6.8 18 | install sdf-glyph-foundry 0.2.0 19 | install gzip-hpp 0.1.0 20 | -------------------------------------------------------------------------------- /src/node_fontnik.cpp: -------------------------------------------------------------------------------- 1 | // fontnik 2 | #include "glyphs.hpp" 3 | #include 4 | //#include 5 | 6 | namespace node_fontnik { 7 | 8 | Napi::Object init(Napi::Env env, Napi::Object exports) { 9 | exports.Set("load", Napi::Function::New(env, Load)); 10 | exports.Set("range", Napi::Function::New(env, Range)); 11 | exports.Set("composite", Napi::Function::New(env, Composite)); 12 | return exports; 13 | } 14 | 15 | // We mark this NOLINT to avoid the clang-tidy checks 16 | // warning about code inside nodejs that we don't control and can't 17 | // directly change to avoid the warning. 18 | NODE_API_MODULE(fontnik, init) // NOLINT 19 | 20 | } // namespace node_fontnik 21 | -------------------------------------------------------------------------------- /fonts/GuardianTextSansWeb/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014 Guardian News & Media Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | ---- 16 | 17 | All fonts are the property of Schwartzco, Inc., t/a Commercial Type 18 | (https://commercialtype.com/), and may not be reproduced without 19 | permission. 20 | -------------------------------------------------------------------------------- /proto/glyphs.proto: -------------------------------------------------------------------------------- 1 | // Protocol Version 1 2 | 3 | syntax = "proto2"; 4 | 5 | package llmr.glyphs; 6 | 7 | option optimize_for = LITE_RUNTIME; 8 | 9 | // Stores a glyph with metrics and optional SDF bitmap information. 10 | message glyph { 11 | required uint32 id = 1; 12 | 13 | // A signed distance field of the glyph with a border of 3 pixels. 14 | optional bytes bitmap = 2; 15 | 16 | // Glyph metrics. 17 | required uint32 width = 3; 18 | required uint32 height = 4; 19 | required sint32 left = 5; 20 | required sint32 top = 6; 21 | required uint32 advance = 7; 22 | } 23 | 24 | // Stores fontstack information and a list of faces. 25 | message fontstack { 26 | required string name = 1; 27 | required string range = 2; 28 | repeated glyph glyphs = 3; 29 | } 30 | 31 | message glyphs { 32 | repeated fontstack stacks = 1; 33 | 34 | extensions 16 to 8191; 35 | } 36 | -------------------------------------------------------------------------------- /bench/bench2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fontnik = require('../'); 5 | var Benchmark = require('benchmark'); 6 | var fs = require('fs'); 7 | 8 | var opensans = fs.readFileSync(path.resolve(__dirname + '/../fonts/open-sans/OpenSans-Regular.ttf')); 9 | 10 | var suite = new Benchmark.Suite(); 11 | 12 | suite 13 | .add('fontnik.load', { 14 | 'defer': true, 15 | 'fn': function(deferred) { 16 | // avoid test inlining 17 | suite.name; 18 | fontnik.load(opensans,function(err) { 19 | if (err) throw err; 20 | deferred.resolve(); 21 | }); 22 | } 23 | }) 24 | .add('fontnik.range', { 25 | 'defer': true, 26 | 'fn': function(deferred) { 27 | // avoid test inlining 28 | suite.name; 29 | fontnik.range({font:opensans,start:0,end:256},function(err) { 30 | if (err) throw err; 31 | deferred.resolve(); 32 | }); 33 | } 34 | }) 35 | .on('cycle', function(event) { 36 | console.log(String(event.target)); 37 | }) 38 | .run({async:true}); 39 | -------------------------------------------------------------------------------- /scripts/clang-format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | : ' 7 | 8 | Runs clang-format on the code in src/ 9 | 10 | Return `1` if there are files to be formatted, and automatically formats them. 11 | 12 | Returns `0` if everything looks properly formatted. 13 | 14 | ' 15 | # Set up the environment by installing mason and clang++ 16 | # See https://github.com/mapbox/node-cpp-skel/blob/master/docs/extended-tour.md#configuration-files 17 | ./scripts/setup.sh --config local.env 18 | source local.env 19 | 20 | # Add clang-format as a dep 21 | mason install clang-format ${MASON_LLVM_RELEASE} 22 | mason link clang-format ${MASON_LLVM_RELEASE} 23 | 24 | # Run clang-format on all cpp and hpp files in the /src directory 25 | find src/ -type f -name '*.hpp' -o -name '*.cpp' \ 26 | | xargs -I{} clang-format -i -style=file {} 27 | 28 | # Print list of modified files 29 | dirty=$(git ls-files --modified src/) 30 | 31 | if [[ $dirty ]]; then 32 | echo "The following files have been modified:" 33 | echo $dirty 34 | git diff 35 | exit 1 36 | else 37 | exit 0 38 | fi -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## `fontnik` 2 | 3 | ### `range(options: object, callback: function)` 4 | 5 | Get a range of glyphs as a protocol buffer. `options` is an object with options: 6 | * `font: buffer` 7 | * `start: number` 8 | * `end: number` 9 | 10 | `font` is the actual font file. 11 | 12 | `callback` will be called as `callback(err, res)` where `res` is the protocol buffer result. 13 | 14 | ### `load(font: buffer, callback: function)` 15 | 16 | Read a font's metadata. Returns an object like 17 | ``` json 18 | "family_name": "Open Sans", 19 | "style_name": "Regular", 20 | "points": [32,33,34,35…] 21 | ``` 22 | where `points` is an array of numbers corresponding to unicode points where this font face has coverage. 23 | 24 | `callback` will be called as `callback(err, res)` where `res` is an array of font style object metadata. 25 | 26 | ### `composite(buffers: [buffer], callback: function)` 27 | 28 | Combine any number of glyph (SDF) PBFs. Returns a re-encoded PBF with the combined font faces, composited using array order to determine glyph priority. 29 | 30 | `callback` will be called as `callback(err, res)` where `res` is the composited protocol buffer result. 31 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | # http://clang.llvm.org/docs/UsersManual.html#profiling-with-instrumentation 7 | # https://www.bignerdranch.com/blog/weve-got-you-covered/ 8 | 9 | # automatically setup environment 10 | 11 | ./scripts/setup.sh --config local.env 12 | source local.env 13 | 14 | make clean 15 | export CXXFLAGS="-fprofile-instr-generate -fcoverage-mapping" 16 | export LDFLAGS="-fprofile-instr-generate" 17 | mason install llvm-cov ${MASON_LLVM_RELEASE} 18 | mason link llvm-cov ${MASON_LLVM_RELEASE} 19 | make debug 20 | rm -f *profraw 21 | rm -f *gcov 22 | rm -f *profdata 23 | LLVM_PROFILE_FILE="code-%p.profraw" npm test 24 | CXX_MODULE="./lib/binding/fontnik.node" 25 | llvm-profdata merge -output=code.profdata code-*.profraw 26 | llvm-cov report ${CXX_MODULE} -instr-profile=code.profdata -use-color 27 | llvm-cov show ${CXX_MODULE} -instr-profile=code.profdata src/*.cpp -filename-equivalence -use-color 28 | llvm-cov show ${CXX_MODULE} -instr-profile=code.profdata src/*.cpp -filename-equivalence -use-color --format html > /tmp/coverage.html 29 | echo "open /tmp/coverage.html for HTML version of this report" 30 | -------------------------------------------------------------------------------- /scripts/sanitize.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | : ' 7 | 8 | Rebuilds the code with the sanitizers and runs the tests 9 | 10 | ' 11 | # Set up the environment by installing mason and clang++ 12 | # See https://github.com/mapbox/node-cpp-skel/blob/master/docs/extended-tour.md#configuration-files 13 | ./scripts/setup.sh --config local.env 14 | source local.env 15 | make clean 16 | export CXXFLAGS="${MASON_SANITIZE_CXXFLAGS} ${CXXFLAGS:-}" 17 | export LDFLAGS="${MASON_SANITIZE_LDFLAGS} ${LDFLAGS:-}" 18 | make debug 19 | export ASAN_OPTIONS=fast_unwind_on_malloc=0:${ASAN_OPTIONS} 20 | if [[ $(uname -s) == 'Darwin' ]]; then 21 | # NOTE: we must call node directly here rather than `npm test` 22 | # because OS X blocks `DYLD_INSERT_LIBRARIES` being inherited by sub shells 23 | # If this is not done right we'll see 24 | # ==18464==ERROR: Interceptors are not working. This may be because AddressSanitizer is loaded too late (e.g. via dlopen). 25 | # 26 | DYLD_INSERT_LIBRARIES=${MASON_LLVM_RT_PRELOAD} \ 27 | node test/*test.js 28 | else 29 | LD_PRELOAD=${MASON_LLVM_RT_PRELOAD} \ 30 | npm test 31 | fi 32 | 33 | -------------------------------------------------------------------------------- /common.gypi: -------------------------------------------------------------------------------- 1 | { 2 | 'target_defaults': { 3 | 'default_configuration': 'Release', 4 | 'cflags_cc' : [ 5 | '-std=c++14' 6 | ], 7 | 'cflags_cc!': ['-std=gnu++0x','-fno-rtti', '-fno-exceptions'], 8 | 'configurations': { 9 | 'Debug': { 10 | 'defines!': [ 11 | 'NDEBUG' 12 | ], 13 | 'cflags_cc!': [ 14 | '-O3', 15 | '-Os', 16 | '-DNDEBUG' 17 | ], 18 | 'xcode_settings': { 19 | 'OTHER_CPLUSPLUSFLAGS!': [ 20 | '-O3', 21 | '-Os', 22 | '-DDEBUG' 23 | ], 24 | 'GCC_OPTIMIZATION_LEVEL': '0', 25 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES' 26 | } 27 | }, 28 | 'Release': { 29 | 'defines': [ 30 | 'NDEBUG' 31 | ], 32 | 'xcode_settings': { 33 | 'OTHER_CPLUSPLUSFLAGS!': [ 34 | '-Os', 35 | '-O2' 36 | ], 37 | 'GCC_OPTIMIZATION_LEVEL': '3', 38 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO', 39 | 'DEAD_CODE_STRIPPING': 'YES', 40 | 'GCC_INLINES_ARE_PRIVATE_EXTERN': 'YES' 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: '*,-llvm-header-guard,-fuchsia*,-modernize-use-trailing-return-type,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers' 3 | WarningsAsErrors: '*' 4 | HeaderFilterRegex: '\/src\/' 5 | AnalyzeTemporaryDtors: false 6 | CheckOptions: 7 | - key: google-readability-braces-around-statements.ShortStatementLines 8 | value: '1' 9 | - key: google-readability-function-size.StatementThreshold 10 | value: '800' 11 | - key: google-readability-namespace-comments.ShortNamespaceLines 12 | value: '10' 13 | - key: google-readability-namespace-comments.SpacesBeforeComments 14 | value: '2' 15 | - key: modernize-loop-convert.MaxCopySize 16 | value: '16' 17 | - key: modernize-loop-convert.MinConfidence 18 | value: reasonable 19 | - key: modernize-loop-convert.NamingStyle 20 | value: CamelCase 21 | - key: modernize-pass-by-value.IncludeStyle 22 | value: llvm 23 | - key: modernize-replace-auto-ptr.IncludeStyle 24 | value: llvm 25 | - key: modernize-use-nullptr.NullMacros 26 | value: 'NULL' 27 | ... 28 | 29 | -------------------------------------------------------------------------------- /bin/build-glyphs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fontnik = require('../index.js'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var { queue } = require('d3-queue'); 7 | 8 | if (process.argv.length !== 4) { 9 | console.log('Usage:'); 10 | console.log(' build-glyphs '); 11 | console.log(''); 12 | console.log('Example:'); 13 | console.log(' build-glyphs ./fonts/open-sans/OpenSans-Regular.ttf ./glyphs'); 14 | process.exit(1); 15 | } 16 | 17 | var fontstack = fs.readFileSync(process.argv[2]); 18 | var dir = path.resolve(process.argv[3]); 19 | 20 | if (!fs.existsSync(dir)) { 21 | console.warn('Error: Directory %s does not exist', dir); 22 | process.exit(1); 23 | } 24 | 25 | var q = queue(Math.max(4, require('os').cpus().length)); 26 | var queue = []; 27 | for (var i = 0; i < 65536; (i = i + 256)) { 28 | q.defer(writeGlyphs, { 29 | font: fontstack, 30 | start: i, 31 | end: Math.min(i + 255, 65535) 32 | }); 33 | } 34 | 35 | function writeGlyphs(opts, done) { 36 | fontnik.range(opts, function (err, zdata) { 37 | if (err) { 38 | console.warn(err.toString()); 39 | process.exit(1); 40 | } 41 | fs.writeFileSync(dir + '/' + opts.start + '-' + opts.end + '.pbf', zdata); 42 | done(); 43 | }); 44 | } 45 | 46 | -------------------------------------------------------------------------------- /scripts/create_scheme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | CONTAINER=build/binding.xcodeproj 7 | OUTPUT="${CONTAINER}/xcshareddata/xcschemes/${SCHEME_NAME}.xcscheme" 8 | 9 | # Required ENV vars: 10 | # - SCHEME_TYPE: type of the scheme 11 | # - SCHEME_NAME: name of the scheme 12 | 13 | # Optional ENV vars: 14 | # - NODE_ARGUMENT (defaults to "") 15 | # - BUILDABLE_NAME (defaults ot SCHEME_NAME) 16 | # - BLUEPRINT_NAME (defaults ot SCHEME_NAME) 17 | 18 | 19 | # Try to reuse the existing Blueprint ID if the scheme already exists. 20 | if [ -f "${OUTPUT}" ]; then 21 | BLUEPRINT_ID=$(sed -n "s/[ \t]*BlueprintIdentifier *= *\"\([A-Z0-9]\{24\}\)\"/\\1/p" "${OUTPUT}" | head -1) 22 | fi 23 | 24 | NODE_ARGUMENT=${NODE_ARGUMENT:-} 25 | BLUEPRINT_ID=${BLUEPRINT_ID:-$(hexdump -n 12 -v -e '/1 "%02X"' /dev/urandom)} 26 | BUILDABLE_NAME=${BUILDABLE_NAME:-${SCHEME_NAME}} 27 | BLUEPRINT_NAME=${BLUEPRINT_NAME:-${SCHEME_NAME}} 28 | 29 | mkdir -p "${CONTAINER}/xcshareddata/xcschemes" 30 | 31 | sed "\ 32 | s#{{BLUEPRINT_ID}}#${BLUEPRINT_ID}#;\ 33 | s#{{BLUEPRINT_NAME}}#${BLUEPRINT_NAME}#;\ 34 | s#{{BUILDABLE_NAME}}#${BUILDABLE_NAME}#;\ 35 | s#{{CONTAINER}}#${CONTAINER}#;\ 36 | s#{{WORKING_DIRECTORY}}#$(pwd)#;\ 37 | s#{{NODE_PATH}}#$(dirname `which node`)#;\ 38 | s#{{NODE_ARGUMENT}}#${NODE_ARGUMENT}#" \ 39 | scripts/${SCHEME_TYPE}.xcscheme > "${OUTPUT}" 40 | -------------------------------------------------------------------------------- /bench/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fontnik = require('../'); 5 | var { queue } = require('d3-queue'); 6 | var fs = require('fs'); 7 | 8 | // https://gist.github.com/mourner/96b1335c6a43e68af252 9 | // https://gist.github.com/fengmk2/4345606 10 | function now() { 11 | var hr = process.hrtime(); 12 | return hr[0] + hr[1] / 1e9; 13 | } 14 | 15 | function bench(opts, cb) { 16 | var q = queue(opts.concurrency); 17 | var start = now(); 18 | for (var i = 1; i <= opts.iterations; i++) { 19 | q.defer.apply({}, opts.args); 20 | } 21 | q.awaitAll(function (error, results) { 22 | var seconds = now() - start; 23 | console.log(opts.name, Math.round(opts.iterations / (seconds)), 'ops/sec', opts.iterations, opts.concurrency); 24 | return cb(); 25 | }); 26 | } 27 | 28 | function main() { 29 | var opensans = fs.readFileSync(path.resolve(__dirname + '/../fonts/open-sans/OpenSans-Regular.ttf')); 30 | 31 | var suite = queue(1); 32 | suite.defer(bench, { 33 | name: "fontnik.load", 34 | args: [fontnik.load, opensans], 35 | iterations: 10, 36 | concurrency: 10 37 | }); 38 | suite.defer(bench, { 39 | name: "fontnik.range", 40 | args: [fontnik.range, { font: opensans, start: 0, end: 256 }], 41 | iterations: 1000, 42 | concurrency: 100 43 | }); 44 | suite.awaitAll(function (err) { 45 | if (err) throw err; 46 | }) 47 | } 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | ### Tagging and publishing binaries via Github Actions 2 | 3 | On each commit that passes through GitHub actions workflow, the binaries are generated for `linux-x64` and `darwin-x64`. These binaries can be downloaded when publishing the npm package. 4 | 5 | Running `npm publish` uses the binaries present in the `prebuilds` directory. When the module is installed with `npm install`, a pre-built binary in the `prebuilds` directory is used if there's one that's suitable for the OS and architecture of the machine. Otherwise, the binary is built from the source when installing. 6 | 7 | Typical workflow: 8 | 9 | ``` 10 | git checkout master 11 | 12 | # increment version number 13 | # https://docs.npmjs.com/cli/version 14 | npm version major | minor | patch 15 | 16 | # amend commit to include "[publish binary]" 17 | git commit --amend 18 | "x.y.z" -> "x.y.z [publish binary]" 19 | 20 | # push commit and tag to remote 21 | git push 22 | git push --tags 23 | 24 | # make a sandwich, check travis console for build successes 25 | # test published binary (should install from remote) 26 | npm install && npm test 27 | 28 | # Make sure that the GHA workflow is successful. Download the artifacts 29 | npm run download-binaries 30 | 31 | # publish to npm 32 | npm publish 33 | ``` 34 | 35 | > Note: gh CLI is required in order to download binaries from GH workflow runs. Follow the instruction in the [link](https://github.com/cli/cli#installation) to install the CLI. Run `gh auth login` and follow the instructions to authenticate before downloading binaries. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Mapbox 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of [project] nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /scripts/generate_compile_commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import json 5 | import os 6 | import re 7 | 8 | # Script to generate compile_commands.json based on Makefile output 9 | # Works by accepting Makefile output from stdin, parsing it, and 10 | # turning into json records. These are then printed to stdout. 11 | # More details on the compile_commands format at: 12 | # https://clang.llvm.org/docs/JSONCompilationDatabase.html 13 | # 14 | # Note: make must be run in verbose mode, e.g. V=1 make or VERBOSE=1 make 15 | # 16 | # Usage with node-cpp-skel: 17 | # 18 | # make | ./scripts/generate_compile_commands.py > build/compile_commands.json 19 | 20 | # These work for node-cpp-skel to detect the files being compiled 21 | # They may need to be modified if you adapt this to another tool 22 | matcher = re.compile('^(.*) (.+cpp)\n') 23 | build_dir = os.path.join(os.getcwd(),"build") 24 | TOKEN_DENOTING_COMPILED_FILE='NODE_GYP_MODULE_NAME' 25 | 26 | def generate(): 27 | compile_commands = [] 28 | for line in sys.stdin.readlines(): 29 | if TOKEN_DENOTING_COMPILED_FILE in line: 30 | match = matcher.match(line) 31 | if match: 32 | matched = match.group(2); 33 | compile_commands.append({ 34 | "directory": build_dir, 35 | "command": line.strip(), 36 | "file": os.path.normpath(os.path.join(build_dir,matched)) 37 | }) 38 | print json.dumps(compile_commands,indent=4) 39 | 40 | if __name__ == '__main__': 41 | generate() 42 | -------------------------------------------------------------------------------- /bin/font-inspect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var { queue } = require('d3-queue'); 6 | var glob = require('glob'); 7 | var argv = require('minimist')(process.argv.slice(2), { 8 | boolean: ['verbose', 'help'] 9 | }); 10 | 11 | if (argv.help) { 12 | console.log('usage: font-inspect --register=FONTDIRECTORY'); 13 | console.log('option: --verbose'); 14 | console.log('option: --register=FONTDIRECTORY'); 15 | console.log('option: --face=SPECIFICFONTFACE'); 16 | return; 17 | } 18 | 19 | if (argv.register && !process.env.FONTNIK_FONTS) { 20 | process.env.FONTNIK_FONTS = path.resolve(argv.register); 21 | } 22 | 23 | var fontnik = require('../index.js'); 24 | var faces = []; 25 | 26 | if (argv.face) { 27 | faces = [argv.face]; 28 | } else { 29 | var register = path.resolve(argv.register); 30 | var pattern = '+(*ttf|*otf)'; 31 | faces = glob.sync(pattern, { nodir: true, cwd: register, matchBase: true }); 32 | faces = faces.map(function (f) { 33 | return path.join(register, f); 34 | }) 35 | } 36 | 37 | 38 | if (argv.verbose) { 39 | console.error('resolved', faces); 40 | } 41 | 42 | var q = queue(); 43 | 44 | function getCoverage(face, cb) { 45 | fs.readFile(face, function (err, res) { 46 | if (err) return cb(err); 47 | fontnik.load(res, function (err, faces) { 48 | if (err) return cb(err); 49 | return cb(null, { 50 | face: [faces[0].family_name, faces[0].style_name].join(' '), 51 | coverage: faces[0].points 52 | }); 53 | }); 54 | }); 55 | } 56 | 57 | faces.forEach(function (f) { q.defer(getCoverage, f) }); 58 | 59 | q.awaitAll(function (err, res) { 60 | if (err) throw err; 61 | process.stdout.write(JSON.stringify(res, null, 2)); 62 | }); 63 | -------------------------------------------------------------------------------- /test/composite.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fontnik = require('../'); 4 | const tape = require('tape'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const protobuf = require('protocol-buffers'); 9 | const messages = protobuf(fs.readFileSync(path.join(__dirname, '../proto/glyphs.proto'))); 10 | const glyphs = messages.glyphs; 11 | 12 | var openSans512 = fs.readFileSync(__dirname + '/fixtures/opensans.512.767.pbf'), 13 | arialUnicode512 = fs.readFileSync(__dirname + '/fixtures/arialunicode.512.767.pbf'), 14 | league512 = fs.readFileSync(__dirname + '/fixtures/league.512.767.pbf'), 15 | composite512 = fs.readFileSync(__dirname + '/fixtures/opensans.arialunicode.512.767.pbf'), 16 | triple512 = fs.readFileSync(__dirname + '/fixtures/league.opensans.arialunicode.512.767.pbf'); 17 | 18 | tape('compositing two pbfs', function(t) { 19 | fontnik.composite([openSans512, arialUnicode512], (err, data) => { 20 | var composite = glyphs.decode(data); 21 | var expected = glyphs.decode(composite512); 22 | 23 | t.ok(composite.stacks, 'has stacks'); 24 | t.equal(composite.stacks.length, 1, 'has one stack'); 25 | 26 | var stack = composite.stacks[0]; 27 | 28 | t.ok(stack.name, 'is a named stack'); 29 | t.ok(stack.range, 'has a glyph range'); 30 | t.deepEqual(composite, expected, 'equals a server-composited stack'); 31 | 32 | composite = glyphs.encode(composite); 33 | expected = glyphs.encode(expected); 34 | 35 | t.deepEqual(composite, expected, 're-encodes nicely'); 36 | 37 | fontnik.composite([league512, composite], (err, data2) => { 38 | var recomposite = glyphs.decode(data2), 39 | reexpect = glyphs.decode(triple512); 40 | 41 | t.deepEqual(recomposite, reexpect, 'can add on a third for good measure'); 42 | 43 | t.end(); 44 | }); 45 | 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 16 | 17 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 18 | 19 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 20 | 21 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 22 | -------------------------------------------------------------------------------- /test/fixtures/fonts-invalid/README.md: -------------------------------------------------------------------------------- 1 | 1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf is from https://github.com/behdad/harfbuzz/blob/7793aad946e09b53523b30d57de85abd1d15f8b6/test/shaping/fonts/sha1sum/1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf 2 | 3 | HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. 4 | For parts of HarfBuzz that are licensed under different licenses see individual 5 | files names COPYING in subdirectories where applicable. 6 | 7 | Copyright © 2010,2011,2012 Google, Inc. 8 | Copyright © 2012 Mozilla Foundation 9 | Copyright © 2011 Codethink Limited 10 | Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) 11 | Copyright © 2009 Keith Stribley 12 | Copyright © 2009 Martin Hosken and SIL International 13 | Copyright © 2007 Chris Wilson 14 | Copyright © 2006 Behdad Esfahbod 15 | Copyright © 2005 David Turner 16 | Copyright © 2004,2007,2008,2009,2010 Red Hat, Inc. 17 | Copyright © 1998-2004 David Turner and Werner Lemberg 18 | 19 | For full copyright notices consult the individual files in the package. 20 | 21 | 22 | Permission is hereby granted, without written agreement and without 23 | license or royalty fees, to use, copy, modify, and distribute this 24 | software and its documentation for any purpose, provided that the 25 | above copyright notice and the following two paragraphs appear in 26 | all copies of this software. 27 | 28 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR 29 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 30 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN 31 | IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 32 | DAMAGE. 33 | 34 | THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, 35 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 36 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 37 | ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO 38 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontnik", 3 | "version": "0.7.4", 4 | "description": "A library that delivers a range of glyphs rendered as SDFs (signed distance fields) in a protobuf.", 5 | "keywords": [ 6 | "font", 7 | "text", 8 | "glyph", 9 | "freetype", 10 | "sdf" 11 | ], 12 | "url": "https://github.com/mapbox/node-fontnik", 13 | "bugs": "https://github.com/mapbox/node-fontnik/issues", 14 | "main": "index.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/mapbox/node-fontnik.git" 18 | }, 19 | "licenses": [ 20 | { 21 | "type": "BSD", 22 | "url": "https://github.com/mapbox/node-fontnik/blob/master/LICENSE.txt" 23 | } 24 | ], 25 | "dependencies": { 26 | "d3-queue": "^3.0.7", 27 | "glob": "^7.1.6", 28 | "minimist": "^1.2.5", 29 | "node-addon-api": "^2.0.1", 30 | "node-gyp-build": "^4.6.0" 31 | }, 32 | "devDependencies": { 33 | "@mapbox/cloudfriend": "^5.1.1", 34 | "aws-sdk": "^2.1227.0", 35 | "benchmark": "^1.0.0", 36 | "mkdirp": "^0.5.1", 37 | "node-gyp": "^9.3.1", 38 | "pbf": "^1.3.5", 39 | "protocol-buffers": "^4.1.0", 40 | "tape": "^4.2.2", 41 | "prebuildify": "^5.0.1" 42 | }, 43 | "bin": { 44 | "build-glyphs": "./bin/build-glyphs", 45 | "font-inspect": "./bin/font-inspect" 46 | }, 47 | "scripts": { 48 | "install": "node-gyp-build", 49 | "test": "./node_modules/.bin/tape test/**/*.test.js", 50 | "prebuildify": "prebuildify --napi --tag-uv --tag-libc --strip", 51 | "download-binaries": "rm -rf prebuilds* && gh run download \"$(gh run list -b \"$(git branch --show-current)\" -L 1 --json databaseId --jq \".[].databaseId\")\" && npm run check-binaries && npm run merge-prebuilds", 52 | "check-binaries": "ls prebuilds-darwin-* >/dev/null 2>&1 && ls prebuilds-linux-* >/dev/null 2>&1 || (echo 'Error: Missing required prebuilds. Check that both darwin and linux prebuilds exist.' && exit 1)", 53 | "merge-prebuilds": "mkdir -p prebuilds && mv prebuilds-*/* prebuilds/ && rm -r prebuilds-*/", 54 | "prepublishOnly": "npm run download-binaries" 55 | }, 56 | "binary": { 57 | "module_name": "fontnik", 58 | "module_path": "./lib/binding/" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULE_NAME := $(shell node -e "console.log(require('./package.json').binary.module_name)") 2 | 3 | # Whether to turn compiler warnings into errors 4 | export WERROR ?= false 5 | 6 | default: release 7 | 8 | ./node_modules/.bin/node-gyp: 9 | # install deps but for now ignore our own install script 10 | # so that we can run it directly in either debug or release 11 | npm install --ignore-scripts 12 | 13 | release: ./node_modules/.bin/node-gyp 14 | V=1 ./node_modules/.bin/node-gyp configure build --error_on_warnings=$(WERROR) --loglevel=error 15 | @echo "run 'make clean' for full rebuild" 16 | 17 | debug: ./node_modules/.bin/node-gyp 18 | V=1 ./node_modules/.bin/node-gyp configure build --error_on_warnings=$(WERROR) --loglevel=error --debug 19 | @echo "run 'make clean' for full rebuild" 20 | 21 | coverage: 22 | ./scripts/coverage.sh 23 | 24 | tidy: 25 | ./scripts/clang-tidy.sh 26 | 27 | format: 28 | ./scripts/clang-format.sh 29 | 30 | sanitize: 31 | ./scripts/sanitize.sh 32 | 33 | clean: 34 | rm -rf lib/binding 35 | rm -rf build 36 | # remove remains from running 'make coverage' 37 | rm -f *.profraw 38 | rm -f *.profdata 39 | @echo "run 'make distclean' to also clear node_modules, mason_packages, and .mason directories" 40 | 41 | distclean: clean 42 | rm -rf node_modules 43 | rm -rf mason_packages 44 | # remove remains from running './scripts/setup.sh' 45 | rm -rf .mason 46 | rm -rf .toolchain 47 | rm -f local.env 48 | rm -rf prebuilds 49 | 50 | xcode: ./node_modules/.bin/node-gyp 51 | ./node_modules/.bin/node-gyp configure -- -f xcode 52 | 53 | @# If you need more targets, e.g. to run other npm scripts, duplicate the last line and change NPM_ARGUMENT 54 | SCHEME_NAME="$(MODULE_NAME)" SCHEME_TYPE=library BLUEPRINT_NAME=$(MODULE_NAME) BUILDABLE_NAME=$(MODULE_NAME).node scripts/create_scheme.sh 55 | SCHEME_NAME="npm test" SCHEME_TYPE=node BLUEPRINT_NAME=$(MODULE_NAME) BUILDABLE_NAME=$(MODULE_NAME).node NODE_ARGUMENT="`npm bin tape`/tape test/*.test.js" scripts/create_scheme.sh 56 | 57 | open build/binding.xcodeproj 58 | 59 | docs: 60 | npm run docs 61 | 62 | test: 63 | npm test 64 | 65 | .PHONY: test docs 66 | 67 | testpack: 68 | rm -f ./*tgz 69 | npm pack 70 | 71 | testpacked: testpack 72 | rm -rf /tmp/package 73 | tar -xf *tgz --directory=/tmp/ 74 | du -h -d 0 /tmp/package 75 | cp -r test /tmp/package/ 76 | cp -r fonts /tmp/package/ 77 | ln -s `pwd`/mason_packages /tmp/package/mason_packages 78 | (cd /tmp/package && make && make test) 79 | -------------------------------------------------------------------------------- /scripts/clang-tidy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | # https://clang.llvm.org/extra/clang-tidy/ 7 | 8 | : ' 9 | 10 | Runs clang-tidy on the code in src/ 11 | 12 | Return `1` if there are files automatically fixed by clang-tidy. 13 | 14 | Returns `0` if no fixes by clang-tidy. 15 | 16 | TODO: should also return non-zero if clang-tidy emits warnings 17 | or errors about things it cannot automatically fix. However I cannot 18 | figure out how to get this working yet as it seems that clang-tidy 19 | always returns 0 even on errors. 20 | 21 | ' 22 | 23 | # to speed up re-runs, only re-create environment if needed 24 | if [[ ! -f local.env ]]; then 25 | # automatically setup environment 26 | ./scripts/setup.sh --config local.env 27 | fi 28 | 29 | # source the environment 30 | source local.env 31 | 32 | PATH_TO_CLANG_TIDY_SCRIPT="$(pwd)/mason_packages/.link/share/run-clang-tidy.py" 33 | 34 | # to speed up re-runs, only install clang-tidy if needed 35 | if [[ ! -f PATH_TO_CLANG_TIDY_SCRIPT ]]; then 36 | # The MASON_LLVM_RELEASE variable comes from `local.env` 37 | mason install clang-tidy ${MASON_LLVM_RELEASE} 38 | # We link the tools to make it easy to know ${PATH_TO_CLANG_TIDY_SCRIPT} 39 | mason link clang-tidy ${MASON_LLVM_RELEASE} 40 | fi 41 | 42 | # build the compile_commands.json file if it does not exist 43 | if [[ ! -f build/compile_commands.json ]]; then 44 | # We need to clean otherwise when we make the project 45 | # will will not see all the compile commands 46 | make clean 47 | # Create the build directory to put the compile_commands in 48 | # We do this first to ensure it is there to start writing to 49 | # immediately (make make not create it right away) 50 | mkdir -p build 51 | # Run make, pipe the output to the generate_compile_commands.py 52 | # and drop them in a place that clang-tidy will automatically find them 53 | make | scripts/generate_compile_commands.py > build/compile_commands.json 54 | fi 55 | 56 | # change into the build directory so that clang-tidy can find the files 57 | # at the right paths (since this is where the actual build happens) 58 | cd build 59 | ${PATH_TO_CLANG_TIDY_SCRIPT} -fix 60 | cd ../ 61 | 62 | # Print list of modified files 63 | dirty=$(git ls-files --modified src/) 64 | 65 | if [[ $dirty ]]; then 66 | echo "The following files have been modified:" 67 | echo $dirty 68 | git diff 69 | exit 1 70 | else 71 | exit 0 72 | fi 73 | 74 | -------------------------------------------------------------------------------- /test/format/glyphs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Glyphs; 4 | function Glyphs(buffer, end) { 5 | // Public 6 | this.stacks = {}; 7 | // Private 8 | this._buffer = buffer; 9 | 10 | var val, tag; 11 | if (typeof end === 'undefined') end = buffer.length; 12 | while (buffer.pos < end) { 13 | val = buffer.readVarint(); 14 | tag = val >> 3; 15 | if (tag == 1) { 16 | var fontstack = this.readFontstack(); 17 | this.stacks[fontstack.name] = fontstack; 18 | } else { 19 | // console.warn('skipping tile tag ' + tag); 20 | buffer.skip(val); 21 | } 22 | } 23 | } 24 | 25 | Glyphs.prototype.readFontstack = function() { 26 | var buffer = this._buffer; 27 | var fontstack = { glyphs: {} }; 28 | 29 | var bytes = buffer.readVarint(); 30 | var val, tag; 31 | var end = buffer.pos + bytes; 32 | while (buffer.pos < end) { 33 | val = buffer.readVarint(); 34 | tag = val >> 3; 35 | 36 | if (tag == 1) { 37 | fontstack.name = buffer.readString(); 38 | } else if (tag == 2) { 39 | var range = buffer.readString(); 40 | fontstack.range = range; 41 | } else if (tag == 3) { 42 | var glyph = this.readGlyph(); 43 | fontstack.glyphs[glyph.id] = glyph; 44 | } else { 45 | buffer.skip(val); 46 | } 47 | } 48 | 49 | return fontstack; 50 | }; 51 | 52 | Glyphs.prototype.readGlyph = function() { 53 | var buffer = this._buffer; 54 | var glyph = {}; 55 | 56 | var bytes = buffer.readVarint(); 57 | var val, tag; 58 | var end = buffer.pos + bytes; 59 | while (buffer.pos < end) { 60 | val = buffer.readVarint(); 61 | tag = val >> 3; 62 | 63 | if (tag == 1) { 64 | glyph.id = buffer.readVarint(); 65 | } else if (tag == 2) { 66 | glyph.bitmap = buffer.readBytes(); 67 | } else if (tag == 3) { 68 | glyph.width = buffer.readVarint(); 69 | } else if (tag == 4) { 70 | glyph.height = buffer.readVarint(); 71 | } else if (tag == 5) { 72 | glyph.left = buffer.readSVarint(); 73 | } else if (tag == 6) { 74 | glyph.top = buffer.readSVarint(); 75 | } else if (tag == 7) { 76 | glyph.advance = buffer.readVarint(); 77 | } else { 78 | buffer.skip(val); 79 | } 80 | } 81 | 82 | return glyph; 83 | }; 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-fontnik 2 | 3 | [![NPM](https://nodei.co/npm/fontnik.png?compact=true)](https://nodei.co/npm/fontnik/) 4 | [![Build Status](https://travis-ci.com/mapbox/node-fontnik.svg?branch=master)](https://travis-ci.com/mapbox/node-fontnik) 5 | [![codecov](https://codecov.io/gh/mapbox/node-fontnik/branch/master/graph/badge.svg)](https://codecov.io/gh/mapbox/node-fontnik) 6 | 7 | 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. 8 | 9 | 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. 10 | 11 | See also [TinySDF](https://github.com/mapbox/tiny-sdf), which is a faster but less precise approach to generating SDFs for fonts. 12 | 13 | ## [API](API.md) 14 | 15 | ## Installing 16 | 17 | By default, installs binaries. On these platforms no external dependencies are needed. 18 | 19 | - 64 bit OS X or 64 bit Linux 20 | - Node.js v8-v16 21 | 22 | Just run: 23 | 24 | ``` 25 | npm install 26 | ``` 27 | 28 | However, other platforms will fall back to a source compile: see [building from source](#building-from-source) for details. 29 | 30 | ## Building from source 31 | 32 | ``` 33 | npm install --build-from-source 34 | ``` 35 | 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`. 36 | 37 | ## Local testing 38 | 39 | Run tests with 40 | 41 | ``` 42 | npm test 43 | ``` 44 | 45 | If you make any changes to the C++ files in the `src/` directory, you'll need to recompile the node bindings (`fontnik.node`) before testing locally: 46 | 47 | ``` 48 | make 49 | ``` 50 | 51 | See the `Makefile` for additional tasks you can run, such as `make coverage`. 52 | 53 | ## Release 54 | See the [Dev doc](./DEV.md) 55 | 56 | ## Background reading 57 | - [Drawing Text with Signed Distance Fields in Mapbox GL](https://www.mapbox.com/blog/text-signed-distance-fields/) 58 | - [State of Text Rendering](http://behdad.org/text/) 59 | - [Pango vs HarfBuzz](http://mces.blogspot.com/2009/11/pango-vs-harfbuzz.html) 60 | - [An Introduction to Writing Systems & Unicode](http://rishida.net/docs/unicode-tutorial/) 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.7.4 4 | - Fix publish with new CI 5 | 6 | ## 0.7.3 7 | 8 | - Moves `prebuildify` to dev dependencies. [#193](https://github.com/mapbox/node-fontnik/pull/193) 9 | 10 | ## 0.7.2 11 | 12 | - Removes `node-pre-gyp` in favor of `prebuildify` to package binaries. [#184](https://github.com/mapbox/node-fontnik/pull/184) 13 | 14 | ## 0.7.1 15 | 16 | - Fixes issue with `point-in-polygon` algorithm being used in `sdf-glyph-foundary` 17 | 18 | ## 0.7.0 19 | 20 | - Adds node v16 support 21 | - Updates vulnerable dependencies 22 | 23 | ## 0.6.0 24 | 25 | - Adds node v12 and v14 support 26 | - Dropped node v4 and v6 support 27 | - Adds `fontnik.composite` 28 | - Drops `libprotobuf` dependency, uses `protozero` instead 29 | - Requires c++14 compatible compiler 30 | - Binaries are published using clang++ 10.0.0 31 | 32 | ## 0.5.2 33 | 34 | - Adds .npmignore to keep downstream node_modules small. 35 | 36 | ## 0.5.1 37 | 38 | - Stopped bundling node-pre-gyp 39 | - Added support for node v8 and v10 40 | - Various performance optimizations and safety checks 41 | 42 | ## 0.5.0 43 | 44 | - Fixed crash on font with null family name 45 | - Optimized the code to reduce memory allocations 46 | - Now using external https://github.com/mapbox/sdf-glyph-foundry 47 | - Now only building binaries for node v4/v6 48 | - Now publishing debug builds for linux 49 | - Now publishing asan builds for linux 50 | - Upgraded from boost 1.62.0 -> 1.63.0 51 | - Upgraded from freetype 2.6 -> 2.7.1 52 | - Upgraded from protobuf 2.6.1 -> 3.2.0 53 | - Moved coverage reporting to codecov.io 54 | 55 | ## 0.4.8 56 | 57 | - Bundles `mkdirp` to avoid an npm@2 bug when using `bundledDependencies` with `devDependencies`. 58 | 59 | ## 0.4.7 60 | 61 | - Upgrades to a modern version of Mason. 62 | 63 | ## 0.4.6 64 | 65 | - Adds prepublish `npm ls` script to prevent publishing without `bundledDependencies`. 66 | 67 | ## 0.4.5 68 | 69 | - Fixes Osaka range segfault. 70 | 71 | ## 0.4.4 72 | 73 | - Fix initialization of `queue-async` in `bin/build-glyphs`. 74 | 75 | ## 0.4.3 76 | 77 | - Handle `ft_face->style_name` null value in `RangeAsync`. 78 | 79 | ## 0.4.2 80 | 81 | - Handle `ft_face->style_name` null value in `LoadAsync`. 82 | 83 | ## 0.4.1 84 | 85 | - Publish Node.js v5.x binaries. 86 | - Autopublish binaries on git tags. 87 | 88 | ## 0.4.0 89 | 90 | - Fixes bounds for short ranges. 91 | - Fixes Travis binary publishing. 92 | - Adds Node.js v4.x support. 93 | 94 | ## 0.2.6 95 | 96 | - Truncate at Unicode point 65535 (0xFFFF) instead of 65533. 97 | 98 | ## 0.2.3 99 | 100 | - Calling .codepoints() on an invalid font will throw a JavaScript 101 | error rather than running into an abort trap. 102 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # Mapbox.Variant C/C+ style 3 | Language: Cpp 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlinesLeft: false 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortIfStatementsOnASingleLine: true 16 | AllowShortLoopsOnASingleLine: true 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: true 22 | BinPackParameters: true 23 | BraceWrapping: 24 | AfterClass: true 25 | AfterControlStatement: true 26 | AfterEnum: true 27 | AfterFunction: true 28 | AfterNamespace: false 29 | AfterObjCDeclaration: true 30 | AfterStruct: true 31 | AfterUnion: true 32 | BeforeCatch: true 33 | BeforeElse: true 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: None 36 | BreakBeforeBraces: Attach 37 | BreakBeforeTernaryOperators: true 38 | BreakConstructorInitializersBeforeComma: false 39 | ColumnLimit: 0 40 | CommentPragmas: '^ IWYU pragma:' 41 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 42 | ConstructorInitializerIndentWidth: 4 43 | ContinuationIndentWidth: 4 44 | Cpp11BracedListStyle: true 45 | DerivePointerAlignment: false 46 | DisableFormat: false 47 | ExperimentalAutoDetectBinPacking: false 48 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 49 | IncludeCategories: 50 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 51 | Priority: 2 52 | - Regex: '^(<|"(gtest|isl|json)/)' 53 | Priority: 3 54 | - Regex: '.*' 55 | Priority: 1 56 | IndentCaseLabels: false 57 | IndentWidth: 4 58 | IndentWrappedFunctionNames: false 59 | KeepEmptyLinesAtTheStartOfBlocks: true 60 | MacroBlockBegin: '' 61 | MacroBlockEnd: '' 62 | MaxEmptyLinesToKeep: 1 63 | NamespaceIndentation: None 64 | ObjCBlockIndentWidth: 2 65 | ObjCSpaceAfterProperty: false 66 | ObjCSpaceBeforeProtocolList: true 67 | PenaltyBreakBeforeFirstCallParameter: 19 68 | PenaltyBreakComment: 300 69 | PenaltyBreakFirstLessLess: 120 70 | PenaltyBreakString: 1000 71 | PenaltyExcessCharacter: 1000000 72 | PenaltyReturnTypeOnItsOwnLine: 60 73 | PointerAlignment: Left 74 | ReflowComments: true 75 | SortIncludes: true 76 | SpaceAfterCStyleCast: false 77 | SpaceBeforeAssignmentOperators: true 78 | SpaceBeforeParens: ControlStatements 79 | SpaceInEmptyParentheses: false 80 | SpacesBeforeTrailingComments: 1 81 | SpacesInAngles: false 82 | SpacesInContainerLiterals: true 83 | SpacesInCStyleCastParentheses: false 84 | SpacesInParentheses: false 85 | SpacesInSquareBrackets: false 86 | Standard: Cpp11 87 | TabWidth: 4 88 | UseTab: Never -------------------------------------------------------------------------------- /scripts/library.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /test/expected/load.json: -------------------------------------------------------------------------------- 1 | [{"family_name":"Fira Sans","style_name":"Medium","points":[32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,402,508,509,510,511,536,537,538,539,567,700,710,711,728,729,730,731,732,733,768,769,770,771,772,774,775,776,778,779,780,787,788,806,807,900,901,902,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1122,1123,1138,1139,1140,1141,1168,1169,1170,1171,1174,1175,1176,1177,1178,1179,1180,1181,1184,1185,1186,1187,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1206,1207,1208,1209,1210,1211,1216,1217,1218,1227,1228,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1308,1309,1316,1317,1318,1319,7808,7809,7810,7811,7812,7813,7922,7923,8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8112,8113,8118,8120,8121,8122,8123,8128,8134,8136,8137,8138,8139,8144,8145,8146,8147,8150,8151,8152,8153,8154,8155,8160,8161,8162,8163,8166,8167,8168,8169,8170,8171,8182,8184,8185,8186,8187,8199,8200,8203,8204,8205,8206,8207,8210,8211,8212,8213,8216,8217,8218,8220,8221,8222,8224,8225,8226,8230,8240,8249,8250,8260,8304,8308,8309,8310,8311,8312,8313,8314,8315,8316,8317,8318,8320,8321,8322,8323,8324,8325,8326,8327,8328,8329,8330,8331,8332,8333,8334,8364,8470,8482,8486,8494,8531,8532,8533,8534,8535,8536,8537,8538,8539,8540,8541,8542,8543,8592,8593,8594,8595,8596,8597,8598,8599,8600,8601,8678,8679,8680,8681,8682,8706,8709,8710,8719,8721,8722,8725,8729,8730,8734,8747,8776,8800,8804,8805,8901,8998,8999,9000,9003,9166,9647,9674,10145,11013,11014,11015,57344,57345,57346,57347,64257,64258,65279,127760]}] 2 | -------------------------------------------------------------------------------- /scripts/node.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 46 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /.github/workflows/builds.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CC: clang 11 | CXX: clang++ 12 | MASON_LLVM_RELEASE: system 13 | PYTHON_VERSION: 3.11 14 | NODE_VERSION: 16 15 | 16 | jobs: 17 | test: 18 | runs-on: ${{ matrix.os.host }} 19 | strategy: 20 | matrix: 21 | node: [16, 18] 22 | build_type: ["debug", "release"] 23 | os: 24 | - name: darwin 25 | architecture: x86-64 26 | host: macos-13 27 | 28 | - name: linux 29 | architecture: x86-64 30 | host: ubuntu-22.04 31 | 32 | name: ${{ matrix.os.name }}-${{ matrix.os.architecture }}-node${{ matrix.node }}-${{ matrix.build_type }} test 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: actions/setup-node@v3 36 | with: 37 | node-version: ${{ matrix.node }} 38 | 39 | - uses: actions/setup-python@v4 40 | if: matrix.os.name == 'darwin' 41 | with: 42 | python-version: ${{ env.PYTHON_VERSION }} 43 | - name: Gyp 44 | if: matrix.os.name == 'darwin' 45 | run: npm install node-gyp@latest 46 | 47 | - name: Test 48 | run: | 49 | npm ci 50 | npm install node-gyp@latest 51 | ./scripts/setup.sh --config local.env 52 | source local.env 53 | make ${{ matrix.build_type }} 54 | npm test 55 | 56 | asan-build-test: 57 | runs-on: ubuntu-22.04 58 | name: ASAN toolset test 59 | env: 60 | BUILDTYPE: debug 61 | TOOLSET: asan 62 | steps: 63 | - uses: actions/checkout@v3 64 | - uses: actions/setup-node@v3 65 | with: 66 | node-version: ${{ env.NODE_VERSION }} 67 | 68 | - name: Test 69 | run: | 70 | npm ci 71 | ./scripts/setup.sh --config local.env 72 | source local.env 73 | export CXXFLAGS="${MASON_SANITIZE_CXXFLAGS} -fno-sanitize-recover=all" 74 | export LDFLAGS="${MASON_SANITIZE_LDFLAGS}" 75 | make ${BUILDTYPE} 76 | export LD_PRELOAD=${MASON_LLVM_RT_PRELOAD} 77 | export ASAN_OPTIONS=fast_unwind_on_malloc=0:${ASAN_OPTIONS} 78 | npm test 79 | unset LD_PRELOAD 80 | 81 | g-build-test: 82 | runs-on: ubuntu-22.04 83 | name: G++ build test 84 | env: 85 | BUILDTYPE: debug 86 | CXX: g++-9 87 | CC: gcc-9 88 | CXXFLAGS: -fext-numeric-literals 89 | steps: 90 | - uses: actions/checkout@v3 91 | - uses: actions/setup-node@v3 92 | with: 93 | node-version: ${{ env.NODE_VERSION }} 94 | 95 | - name: Test 96 | run: | 97 | npm ci 98 | ./scripts/setup.sh --config local.env 99 | source local.env 100 | make ${BUILDTYPE} 101 | npm test 102 | 103 | build: 104 | needs: [test, asan-build-test, g-build-test] 105 | runs-on: ${{ matrix.os.host }} 106 | strategy: 107 | matrix: 108 | os: 109 | - name: darwin 110 | architecture: x86-64 111 | host: macos-13 112 | 113 | - name: linux 114 | architecture: x86-64 115 | host: ubuntu-22.04 116 | 117 | steps: 118 | - uses: actions/checkout@v3 119 | - uses: actions/setup-node@v3 120 | with: 121 | node-version: ${{ env.NODE_VERSION }} 122 | 123 | - uses: actions/setup-python@v4 124 | if: matrix.os.name == 'darwin' 125 | with: 126 | python-version: ${{ env.PYTHON_VERSION }} 127 | - name: Gyp 128 | if: matrix.os.name == 'darwin' 129 | run: npm install node-gyp@latest 130 | 131 | - name: Build 132 | run: | 133 | npm ci 134 | ./scripts/setup.sh --config local.env 135 | source local.env 136 | make release 137 | 138 | - name: Prebuildify ${{ matrix.os.name }}-${{ matrix.os.architecture }} 139 | run: npm run prebuildify -- --platform=${{ matrix.os.name }} --arch=x64 140 | 141 | # Upload the end-user binary artifact 142 | - uses: actions/upload-artifact@v4 143 | with: 144 | name: prebuilds-${{ matrix.os.name }}-${{ matrix.os.architecture }} 145 | path: prebuilds 146 | retention-days: 14 147 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | # This file inherits default targets for Node addons, see https://github.com/nodejs/node-gyp/blob/master/addon.gypi 2 | { 3 | 'includes': [ 'common.gypi' ], # brings in a default set of options that are inherited from gyp 4 | 'variables': { # custom variables we use specific to this file 5 | 'error_on_warnings%':'true', # can be overriden by a command line variable because of the % sign using "WERROR" (defined in Makefile) 6 | # Use this variable to silence warnings from mason dependencies and from N-API 7 | # It's a variable to make easy to pass to 8 | # cflags (linux) and xcode (mac) 9 | 'system_includes': [ 10 | "-isystem -1; 34 | }).length, files.length, 'all .pbfs'); 35 | q.end(); 36 | }) 37 | }); 38 | }); 39 | t.test('errors on invalid font', function (q) { 40 | exec([script, path.join(registry_invalid, '1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf'), dir].join(' '), function (err, stdout, stderr) { 41 | q.ok(err); 42 | q.ok(err.message.indexOf('font does not have family_name') > -1); 43 | q.end(); 44 | }); 45 | }); 46 | t.end(); 47 | }); 48 | 49 | test('bin/font-inspect', function (t) { 50 | var script = path.normalize(__dirname + '/../bin/font-inspect'), 51 | opensans = path.normalize(__dirname + '/fixtures/fonts/OpenSans-Regular.ttf'), 52 | firasans = path.normalize(__dirname + '/fixtures/fonts/FiraSans-Medium.ttf'); 53 | 54 | t.test(' --face', function (q) { 55 | exec([script, '--face=' + opensans].join(' '), function (err, stdout, stderr) { 56 | q.error(err); 57 | if (!process.env.TOOLSET) q.error(stderr); 58 | q.ok(stdout.length, 'outputs to console'); 59 | var output = JSON.parse(stdout); 60 | q.equal(output.length, 1, 'single face'); 61 | q.equal(output[0].face, 'Open Sans Regular'); 62 | q.ok(Array.isArray(output[0].coverage)); 63 | q.equal(output[0].coverage.length, 882); 64 | q.end(); 65 | }); 66 | }); 67 | 68 | t.test(' --register', function (q) { 69 | exec([script, '--register=' + registry].join(' '), function (err, stdout, stderr) { 70 | q.error(err); 71 | if (!process.env.TOOLSET) q.error(stderr); 72 | q.ok(stdout.length, 'outputs to console'); 73 | var output = JSON.parse(stdout); 74 | q.equal(output.length, 2, 'both faces in register'); 75 | q.equal(output[0].face, 'Fira Sans Medium', 'Fira Sans Medium'); 76 | q.ok(Array.isArray(output[0].coverage), 'codepoints array'); 77 | q.equal(output[1].face, 'Open Sans Regular', 'Open Sans Regular'); 78 | q.ok(Array.isArray(output[1].coverage), 'codepoints array'); 79 | q.end(); 80 | }); 81 | }); 82 | 83 | t.test(' --register --verbose', function (q) { 84 | exec([script, '--verbose', '--register=' + registry].join(' '), function (err, stdout, stderr) { 85 | q.error(err); 86 | q.ok(stderr.length, 'writes verbose output to stderr'); 87 | if (!process.env.TOOLSET) { 88 | q.equal(stderr.indexOf('resolved'), 0); 89 | var verboseOutput = JSON.parse(stderr.slice(9).trim().replace(/'/g, '"')); 90 | t.equal(verboseOutput.length, 2); 91 | t.equal(verboseOutput.filter(function (f) { return f.indexOf('.ttf') > -1; }).length, 2); 92 | } 93 | q.ok(stdout.length, 'writes codepoints output to stdout'); 94 | q.ok(JSON.parse(stdout)); 95 | q.end(); 96 | }); 97 | }); 98 | 99 | t.test(' --register --verbose', function (q) { 100 | exec([script, '--verbose', '--register=' + registry_invalid].join(' '), function (err, stdout, stderr) { 101 | q.ok(err); 102 | q.ok(stderr.indexOf('font does not have family_name or style_name') > -1); 103 | q.end(); 104 | }); 105 | }); 106 | 107 | t.end(); 108 | }); 109 | 110 | test('teardown', function (t) { 111 | var q = queue(); 112 | 113 | fs.readdir(bin_output, function (err, files) { 114 | files.forEach(function (f) { 115 | q.defer(fs.unlink, path.join(bin_output, '/', f)); 116 | }); 117 | 118 | q.awaitAll(function (err) { 119 | t.error(err, 'teardown'); 120 | fs.rmdir(bin_output, function (err) { 121 | t.error(err, 'teardown'); 122 | t.end(); 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | export MASON_RELEASE="${MASON_RELEASE:-master}" 7 | export MASON_LLVM_RELEASE="${MASON_LLVM_RELEASE:-10.0.0}" 8 | 9 | PLATFORM=$(uname | tr A-Z a-z) 10 | if [[ ${PLATFORM} == 'darwin' ]]; then 11 | PLATFORM="osx" 12 | fi 13 | 14 | MASON_URL="https://s3.amazonaws.com/mason-binaries/${PLATFORM}-$(uname -m)" 15 | 16 | llvm_toolchain_dir="$(pwd)/.toolchain" 17 | 18 | function run() { 19 | local config=${1} 20 | # unbreak bash shell due to rvm bug on osx: https://github.com/direnv/direnv/issues/210#issuecomment-203383459 21 | # this impacts any usage of scripts that are source'd (like this one) 22 | if [[ "${TRAVIS_OS_NAME:-}" == "osx" ]]; then 23 | echo 'shell_session_update() { :; }' > ~/.direnvrc 24 | fi 25 | 26 | # 27 | # COMPILER TOOLCHAIN 28 | # 29 | 30 | # We install clang++ without the mason client for a couple reasons: 31 | # 1) decoupling makes it viable to use a custom branch of mason that might 32 | # modify the upstream s3 bucket in a such a way that does not give 33 | # it access to build tools like clang++ 34 | # 2) Allows us to short-circuit and use a global clang++ install if it 35 | # is available to save space for local builds. 36 | GLOBAL_CLANG="${HOME}/.mason/mason_packages/${PLATFORM}-$(uname -m)/clang++/${MASON_LLVM_RELEASE}" 37 | GLOBAL_LLVM="${HOME}/.mason/mason_packages/${PLATFORM}-$(uname -m)/llvm/${MASON_LLVM_RELEASE}" 38 | if [[ -d ${GLOBAL_LLVM} ]]; then 39 | echo "Detected '${GLOBAL_LLVM}/bin/clang++', using it" 40 | local llvm_toolchain_dir=${GLOBAL_LLVM} 41 | elif [[ -d ${GLOBAL_CLANG} ]]; then 42 | echo "Detected '${GLOBAL_CLANG}/bin/clang++', using it" 43 | local llvm_toolchain_dir=${GLOBAL_CLANG} 44 | elif [[ -d ${GLOBAL_CLANG} ]]; then 45 | echo "Detected '${GLOBAL_CLANG}/bin/clang++', using it" 46 | local llvm_toolchain_dir=${GLOBAL_CLANG} 47 | elif [[ "${MASON_LLVM_RELEASE}" == "system" ]]; then 48 | echo "Using system clang++ instead of downloading custom toolchain" 49 | llvm_toolchain_dir="/usr" 50 | elif [[ ! -d ${llvm_toolchain_dir} ]]; then 51 | BINARY="${MASON_URL}/clang++/${MASON_LLVM_RELEASE}.tar.gz" 52 | echo "Downloading ${BINARY}" 53 | mkdir -p ${llvm_toolchain_dir} 54 | curl -sSfL ${BINARY} | tar --gunzip --extract --strip-components=1 --directory=${llvm_toolchain_dir} 55 | fi 56 | 57 | # 58 | # MASON 59 | # 60 | 61 | function setup_mason() { 62 | local install_dir=${1} 63 | local mason_release=${2} 64 | mkdir -p ${install_dir} 65 | curl -sSfL https://github.com/mapbox/mason/archive/${mason_release}.tar.gz | tar --gunzip --extract --strip-components=1 --directory=${install_dir} 66 | } 67 | 68 | setup_mason $(pwd)/.mason ${MASON_RELEASE} 69 | 70 | # 71 | # ENV SETTINGS 72 | # 73 | 74 | echo "export PATH=${llvm_toolchain_dir}/bin:$(pwd)/.mason:$(pwd)/mason_packages/.link/bin:"'${PATH}' > ${config} 75 | echo "export CXX=${CXX:-${llvm_toolchain_dir}/bin/clang++}" >> ${config} 76 | echo "export MASON_RELEASE=${MASON_RELEASE}" >> ${config} 77 | echo "export MASON_LLVM_RELEASE=${MASON_LLVM_RELEASE}" >> ${config} 78 | # https://github.com/google/sanitizers/wiki/AddressSanitizerAsDso 79 | RT_BASE=${llvm_toolchain_dir}/lib/clang/${MASON_LLVM_RELEASE}/lib/$(uname | tr A-Z a-z)/libclang_rt 80 | if [[ $(uname -s) == 'Darwin' ]]; then 81 | RT_PRELOAD=${RT_BASE}.asan_osx_dynamic.dylib 82 | else 83 | RT_PRELOAD=${RT_BASE}.asan-x86_64.so 84 | fi 85 | echo "export MASON_LLVM_RT_PRELOAD=${RT_PRELOAD}" >> ${config} 86 | SUPPRESSION_FILE="/tmp/leak_suppressions.txt" 87 | echo "leak:__strdup" > ${SUPPRESSION_FILE} 88 | echo "leak:v8::internal" >> ${SUPPRESSION_FILE} 89 | echo "leak:node::CreateEnvironment" >> ${SUPPRESSION_FILE} 90 | echo "leak:node::Init" >> ${SUPPRESSION_FILE} 91 | echo "leak:node::Buffer::Copy" >> ${SUPPRESSION_FILE} 92 | echo "export ASAN_SYMBOLIZER_PATH=${llvm_toolchain_dir}/bin/llvm-symbolizer" >> ${config} 93 | echo "export MSAN_SYMBOLIZER_PATH=${llvm_toolchain_dir}/bin/llvm-symbolizer" >> ${config} 94 | echo "export UBSAN_OPTIONS=print_stacktrace=1" >> ${config} 95 | echo "export LSAN_OPTIONS=suppressions=${SUPPRESSION_FILE}" >> ${config} 96 | echo "export ASAN_OPTIONS=detect_leaks=1:symbolize=1:abort_on_error=1:detect_container_overflow=1:check_initialization_order=1:detect_stack_use_after_return=1" >> ${config} 97 | echo 'export MASON_SANITIZE="-fsanitize=address,undefined,integer,leak -fno-sanitize=vptr,function"' >> ${config} 98 | echo 'export MASON_SANITIZE_CXXFLAGS="${MASON_SANITIZE} -fno-sanitize=vptr,function -fsanitize-address-use-after-scope -fno-omit-frame-pointer -fno-common"' >> ${config} 99 | echo 'export MASON_SANITIZE_LDFLAGS="${MASON_SANITIZE}"' >> ${config} 100 | 101 | exit 0 102 | } 103 | 104 | function usage() { 105 | >&2 echo "Usage" 106 | >&2 echo "" 107 | >&2 echo "$ ./scripts/setup.sh --config local.env" 108 | >&2 echo "$ source local.env" 109 | >&2 echo "" 110 | exit 1 111 | } 112 | 113 | if [[ ! ${1:-} ]]; then 114 | usage 115 | fi 116 | 117 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 118 | for i in "$@" 119 | do 120 | case $i in 121 | --config) 122 | if [[ ! ${2:-} ]]; then 123 | usage 124 | fi 125 | shift 126 | run $@ 127 | ;; 128 | -h | --help) 129 | usage 130 | shift 131 | ;; 132 | *) 133 | usage 134 | ;; 135 | esac 136 | done 137 | -------------------------------------------------------------------------------- /test/fontnik.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint node: true */ 4 | 5 | var fontnik = require('..'); 6 | var test = require('tape'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var zlib = require('zlib'); 10 | var zdata = fs.readFileSync(__dirname + '/fixtures/range.0.256.pbf'); 11 | var Protobuf = require('pbf'); 12 | var Glyphs = require('./format/glyphs'); 13 | var UPDATE = process.env.UPDATE; 14 | 15 | function nobuffer(key, val) { 16 | return key !== '_buffer' && key !== 'bitmap' ? val : undefined; 17 | } 18 | 19 | function jsonEqual(t, key, json) { 20 | if (UPDATE) fs.writeFileSync(__dirname + '/expected/' + key + '.json', JSON.stringify(json, null, 2)); 21 | t.deepEqual(json, require('./expected/' + key + '.json')); 22 | } 23 | 24 | var expected = JSON.parse(fs.readFileSync(__dirname + '/expected/load.json').toString()); 25 | var firasans = fs.readFileSync(path.resolve(__dirname + '/../fonts/firasans-medium/FiraSans-Medium.ttf')); 26 | var opensans = fs.readFileSync(path.resolve(__dirname + '/../fonts/open-sans/OpenSans-Regular.ttf')); 27 | var invalid_no_family = fs.readFileSync(path.resolve(__dirname + '/fixtures/fonts-invalid/1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf')); 28 | var guardianbold = fs.readFileSync(path.resolve(__dirname + '/../fonts/GuardianTextSansWeb/GuardianTextSansWeb-Bold.ttf')); 29 | var osaka = fs.readFileSync(path.resolve(__dirname + '/../fonts/osaka/Osaka.ttf')); 30 | 31 | test('load', function(t) { 32 | t.test('loads: Fira Sans', function(t) { 33 | fontnik.load(firasans, function(err, faces) { 34 | t.error(err); 35 | t.equal(faces[0].points.length, 789); 36 | t.equal(faces[0].family_name, 'Fira Sans'); 37 | t.equal(faces[0].style_name, 'Medium'); 38 | t.end(); 39 | }); 40 | }); 41 | 42 | t.test('loads: Open Sans', function(t) { 43 | fontnik.load(opensans, function(err, faces) { 44 | t.error(err); 45 | t.equal(faces[0].points.length, 882); 46 | t.equal(faces[0].family_name, 'Open Sans'); 47 | t.equal(faces[0].style_name, 'Regular'); 48 | t.end(); 49 | }); 50 | }); 51 | 52 | t.test('loads: Guardian Bold', function(t) { 53 | fontnik.load(guardianbold, function(err, faces) { 54 | t.error(err); 55 | t.equal(faces[0].points.length, 227); 56 | t.equal(faces[0].hasOwnProperty('family_name'), true); 57 | t.equal(faces[0].family_name, '?'); 58 | t.equal(faces[0].hasOwnProperty('style_name'), false); 59 | t.equal(faces[0].style_name, undefined); 60 | t.end(); 61 | }); 62 | }); 63 | 64 | t.test('loads: Osaka', function(t) { 65 | fontnik.load(osaka, function(err, faces) { 66 | t.error(err); 67 | t.equal(faces[0].family_name, 'Osaka'); 68 | t.equal(faces[0].style_name, 'Regular'); 69 | t.end(); 70 | }); 71 | }); 72 | 73 | t.test('invalid arguments', function(t) { 74 | t.throws(function() { 75 | fontnik.load(); 76 | }, /First argument must be a font buffer/); 77 | 78 | t.throws(function() { 79 | fontnik.load({}); 80 | }, /First argument must be a font buffer/); 81 | 82 | t.end(); 83 | }); 84 | 85 | t.test('non existent font loading', function(t) { 86 | var doesnotexistsans = Buffer.from('baloney'); 87 | fontnik.load(doesnotexistsans, function(err, faces) { 88 | t.ok(err.message.indexOf('Font buffer is not an object')); 89 | t.end(); 90 | }); 91 | }); 92 | 93 | t.test('load typeerror callback', function(t) { 94 | t.throws(function() { 95 | fontnik.load(firasans); 96 | }, /Callback must be a function/); 97 | t.end(); 98 | }); 99 | 100 | t.test('load font with no family name', function(t) { 101 | fontnik.load(invalid_no_family, function(err, faces) { 102 | t.ok(err.message.indexOf('font does not have family_name') > -1); 103 | t.equal(faces,undefined); 104 | t.end(); 105 | }); 106 | }); 107 | 108 | }); 109 | 110 | test('range', function(t) { 111 | var data; 112 | zlib.inflate(zdata, function(err, d) { 113 | if (err) throw err; 114 | data = d; 115 | }); 116 | 117 | t.test('ranges', function(t) { 118 | fontnik.range({font: opensans, start: 0, end: 256}, function(err, res) { 119 | t.error(err); 120 | t.ok(data); 121 | 122 | var zpath = __dirname + '/fixtures/range.0.256.pbf'; 123 | 124 | function compare() { 125 | zlib.inflate(fs.readFileSync(zpath), function(err, inflated) { 126 | t.error(err); 127 | t.deepEqual(data, inflated); 128 | 129 | var vt = new Glyphs(new Protobuf(new Uint8Array(data))); 130 | var json = JSON.parse(JSON.stringify(vt, nobuffer)); 131 | jsonEqual(t, 'range', json); 132 | 133 | t.end(); 134 | }); 135 | } 136 | 137 | if (UPDATE) { 138 | zlib.deflate(data, function(err, zdata) { 139 | t.error(err); 140 | fs.writeFileSync(zpath, zdata); 141 | compare(); 142 | }); 143 | } else { 144 | compare(); 145 | } 146 | }); 147 | }); 148 | 149 | t.test('longrange', function(t) { 150 | fontnik.range({font: opensans, start: 0, end: 1024}, function(err, data) { 151 | t.error(err); 152 | t.ok(data); 153 | t.end(); 154 | }); 155 | }); 156 | 157 | t.test('shortrange', function(t) { 158 | fontnik.range({font: opensans, start: 34, end: 38}, function(err, res) { 159 | t.error(err); 160 | var vt = new Glyphs(new Protobuf(new Uint8Array(res))); 161 | t.equal(vt.stacks.hasOwnProperty('Open Sans Regular'), true); 162 | var codes = Object.keys(vt.stacks['Open Sans Regular'].glyphs); 163 | t.deepEqual(codes, ['34','35','36','37','38']); 164 | t.end(); 165 | }); 166 | }); 167 | 168 | t.test('invalid arguments', function(t) { 169 | t.throws(function() { 170 | fontnik.range(); 171 | }, /First argument must be an object of options/); 172 | 173 | t.throws(function() { 174 | fontnik.range({font:'not an object'}, function(err, data) {}); 175 | }, /Font buffer is not an object/); 176 | 177 | t.throws(function() { 178 | fontnik.range({font:{}}, function(err, data) {}); 179 | }, /First argument must be a font buffer/); 180 | 181 | t.end(); 182 | }); 183 | 184 | t.test('range filepath does not exist', function(t) { 185 | var doesnotexistsans = Buffer.from('baloney'); 186 | fontnik.range({font: doesnotexistsans, start: 0, end: 256}, function(err, faces) { 187 | t.ok(err); 188 | t.equal(err.message, 'could not open font'); 189 | t.end(); 190 | }); 191 | }); 192 | 193 | t.test('range invalid font with no family name', function(t) { 194 | fontnik.range({font: invalid_no_family, start: 0, end: 256}, function(err, faces) { 195 | t.ok(err); 196 | t.equal(err.message, 'font does not have family_name'); 197 | t.end(); 198 | }); 199 | }); 200 | 201 | t.test('range typeerror start', function(t) { 202 | t.throws(function() { 203 | fontnik.range({font: opensans, start: 'x', end: 256}, function(err, data) {}); 204 | }, /option `start` must be a number from 0-65535/); 205 | t.throws(function() { 206 | fontnik.range({font: opensans, start: -3, end: 256}, function(err, data) {}); 207 | }, /option `start` must be a number from 0-65535/); 208 | t.end(); 209 | }); 210 | 211 | t.test('range typeerror end', function(t) { 212 | t.throws(function() { 213 | fontnik.range({font: opensans, start: 0, end: 'y'}, function(err, data) {}); 214 | }, /option `end` must be a number from 0-65535/); 215 | t.throws(function() { 216 | fontnik.range({font: opensans, start: 0, end: 10000000}, function(err, data) {}); 217 | }, /option `end` must be a number from 0-65535/); 218 | t.end(); 219 | }); 220 | 221 | t.test('range typeerror lt', function(t) { 222 | t.throws(function() { 223 | fontnik.range({font: opensans, start: 256, end: 0}, function(err, data) {}); 224 | }, /`start` must be less than or equal to `end`/); 225 | t.end(); 226 | }); 227 | 228 | t.test('range typeerror callback', function(t) { 229 | t.throws(function() { 230 | fontnik.range({font: opensans, start: 0, end: 256}, ''); 231 | }, /Callback must be a function/); 232 | t.throws(function() { 233 | fontnik.range({font: opensans, start: 0, end: 256}); 234 | }, /Callback must be a function/); 235 | t.end(); 236 | }); 237 | 238 | t.test('range with undefined style_name', function(t) { 239 | fontnik.range({font: guardianbold, start: 0, end: 256}, function(err, data) { 240 | t.error(err); 241 | var vt = new Glyphs(new Protobuf(new Uint8Array(data))); 242 | t.equal(vt.stacks.hasOwnProperty('?'), true); 243 | t.equal(vt.stacks['?'].hasOwnProperty('name'), true); 244 | t.equal(vt.stacks['?'].name, '?'); 245 | t.end(); 246 | }); 247 | }); 248 | 249 | t.test('range with osaka', function(t) { 250 | fontnik.range({font: osaka, start:0, end: 256}, function(err, data) { 251 | t.error(err); 252 | var vt = new Glyphs(new Protobuf(new Uint8Array(data))); 253 | var glyphs = vt.stacks['Osaka Regular'].glyphs; 254 | var keys = Object.keys(glyphs); 255 | 256 | var glyph; 257 | for (var i = 0; i < keys.length; i++) { 258 | glyph = glyphs[keys[i]]; 259 | t.deepEqual(Object.keys(glyph), ['id', 'width', 'height', 'left', 'top', 'advance']); 260 | t.equal(glyph.width, 0); 261 | t.equal(glyph.height, 0); 262 | } 263 | 264 | t.end(); 265 | }); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /fonts/open-sans/Apache License.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/glyphs.cpp: -------------------------------------------------------------------------------- 1 | // fontnik 2 | #include "glyphs.hpp" 3 | #include 4 | #include 5 | // node 6 | #include 7 | #include 8 | #include 9 | // sdf-glyph-foundry 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace node_fontnik { 18 | 19 | struct FaceMetadata { 20 | // non copyable 21 | FaceMetadata(FaceMetadata const&) = delete; 22 | FaceMetadata& operator=(FaceMetadata const&) = delete; 23 | // movaable only for highest efficiency 24 | FaceMetadata& operator=(FaceMetadata&& c) = default; 25 | FaceMetadata(FaceMetadata&& c) = default; 26 | 27 | std::string family_name{}; 28 | std::string style_name{}; 29 | std::vector points{}; 30 | FaceMetadata(std::string _family_name, 31 | std::string _style_name, 32 | std::vector&& _points) 33 | : family_name(std::move(_family_name)), 34 | style_name(std::move(_style_name)), 35 | points(std::move(_points)) {} 36 | FaceMetadata(std::string _family_name, 37 | std::vector&& _points) 38 | : family_name(std::move(_family_name)), 39 | points(std::move(_points)) {} 40 | }; 41 | 42 | struct GlyphPBF { 43 | explicit GlyphPBF(Napi::Buffer const& buffer) 44 | : data{buffer.As>().Data(), 45 | buffer.As>().Length()}, 46 | buffer_ref_{Napi::Persistent(buffer)} {} 47 | 48 | // non-copyable 49 | GlyphPBF(GlyphPBF const&) = delete; 50 | GlyphPBF& operator=(GlyphPBF const&) = delete; 51 | 52 | // non-movable 53 | GlyphPBF(GlyphPBF&&) = delete; 54 | GlyphPBF& operator=(GlyphPBF&&) = delete; 55 | 56 | protozero::data_view data; 57 | Napi::Reference> buffer_ref_; 58 | }; 59 | 60 | struct ft_library_guard { 61 | // non copyable 62 | ft_library_guard(ft_library_guard const&) = delete; 63 | ft_library_guard& operator=(ft_library_guard const&) = delete; 64 | 65 | explicit ft_library_guard(FT_Library* lib) : library_(lib) {} 66 | 67 | ~ft_library_guard() { 68 | if (library_ != nullptr) { 69 | FT_Done_FreeType(*library_); 70 | } 71 | } 72 | 73 | FT_Library* library_; 74 | }; 75 | 76 | struct ft_face_guard { 77 | // non copyable 78 | ft_face_guard(ft_face_guard const&) = delete; 79 | ft_face_guard& operator=(ft_face_guard const&) = delete; 80 | explicit ft_face_guard(FT_Face* f) : face_(f) {} 81 | 82 | ~ft_face_guard() { 83 | if (face_ != nullptr) { 84 | FT_Done_Face(*face_); 85 | } 86 | } 87 | 88 | FT_Face* face_; 89 | }; 90 | 91 | struct AsyncLoad : Napi::AsyncWorker { 92 | using Base = Napi::AsyncWorker; 93 | AsyncLoad(Napi::Buffer const& buffer, Napi::Function const& callback) 94 | : Base(callback), 95 | font_data_{buffer.Data()}, 96 | font_size_{buffer.Length()}, 97 | buffer_ref_{Napi::Persistent(buffer)} {} 98 | 99 | void Execute() override { 100 | try { 101 | FT_Library library = nullptr; 102 | ft_library_guard library_guard(&library); 103 | FT_Error error = FT_Init_FreeType(&library); 104 | if (error != 0) { 105 | //LCOV_EXCL_START 106 | SetError("could not open FreeType library"); 107 | return; 108 | // LCOV_EXCL_END 109 | } 110 | FT_Face ft_face = nullptr; 111 | FT_Long num_faces = 0; 112 | for (int i = 0; ft_face == nullptr || i < num_faces; ++i) { 113 | ft_face_guard face_guard(&ft_face); 114 | FT_Error face_error = FT_New_Memory_Face(library, 115 | reinterpret_cast(font_data_), 116 | static_cast(font_size_), i, &ft_face); 117 | if (face_error != 0) { 118 | SetError("could not open font file"); 119 | return; 120 | } 121 | if (num_faces == 0) { 122 | num_faces = ft_face->num_faces; 123 | faces_.reserve(static_cast(num_faces)); 124 | } 125 | if (ft_face->family_name != nullptr) { 126 | std::set points; 127 | FT_ULong charcode; 128 | FT_UInt gindex; 129 | charcode = FT_Get_First_Char(ft_face, &gindex); 130 | while (gindex != 0) { 131 | charcode = FT_Get_Next_Char(ft_face, charcode, &gindex); 132 | if (charcode != 0) points.emplace(charcode); 133 | } 134 | std::vector points_vec(points.begin(), points.end()); 135 | if (ft_face->style_name != nullptr) { 136 | faces_.emplace_back(ft_face->family_name, ft_face->style_name, std::move(points_vec)); 137 | } else { 138 | faces_.emplace_back(ft_face->family_name, std::move(points_vec)); 139 | } 140 | } else { 141 | SetError("font does not have family_name or style_name"); 142 | return; 143 | } 144 | } 145 | } catch (std::exception const& ex) { 146 | SetError(ex.what()); 147 | } 148 | } 149 | 150 | std::vector GetResult(Napi::Env env) override { 151 | Napi::Array js_faces = Napi::Array::New(env, faces_.size()); 152 | std::uint32_t index = 0; 153 | for (auto const& face : faces_) { 154 | Napi::Object js_face = Napi::Object::New(env); 155 | js_face.Set("family_name", face.family_name); 156 | if (!face.style_name.empty()) { 157 | js_face.Set("style_name", face.style_name); 158 | } 159 | Napi::Array js_points = Napi::Array::New(env, face.points.size()); 160 | std::uint32_t p_idx = 0; 161 | for (auto const& pt : face.points) { 162 | js_points.Set(p_idx++, pt); 163 | } 164 | js_face.Set("points", js_points); 165 | js_faces.Set(index++, js_face); 166 | } 167 | return {env.Null(), js_faces}; 168 | } 169 | 170 | private: 171 | char const* font_data_; 172 | std::size_t font_size_; 173 | Napi::Reference> buffer_ref_; 174 | std::vector faces_; 175 | }; 176 | 177 | struct AsyncRange : Napi::AsyncWorker { 178 | using Base = Napi::AsyncWorker; 179 | AsyncRange(Napi::Buffer const& buffer, std::uint32_t start, std::uint32_t end, Napi::Function const& callback) 180 | : Base(callback), 181 | font_data_{buffer.Data()}, 182 | font_size_{buffer.Length()}, 183 | start_(start), 184 | end_(end), 185 | buffer_ref_{Napi::Persistent(buffer)} {} 186 | 187 | void Execute() override { 188 | try { 189 | unsigned array_size = end_ - start_; 190 | chars_.reserve(array_size); 191 | for (unsigned i = start_; i <= end_; ++i) { 192 | chars_.emplace_back(i); 193 | } 194 | 195 | FT_Library library = nullptr; 196 | ft_library_guard library_guard(&library); 197 | FT_Error error = FT_Init_FreeType(&library); 198 | if (error != 0) { 199 | // LCOV_EXCL_START 200 | SetError("could not open FreeType library"); 201 | return; 202 | // LCOV_EXCL_END 203 | } 204 | 205 | protozero::pbf_writer pbf_writer{message_}; 206 | FT_Face ft_face = nullptr; 207 | FT_Long num_faces = 0; 208 | for (int i = 0; ft_face == nullptr || i < num_faces; ++i) { 209 | ft_face_guard face_guard(&ft_face); 210 | FT_Error face_error = FT_New_Memory_Face(library, 211 | reinterpret_cast(font_data_), 212 | static_cast(font_size_), i, &ft_face); 213 | if (face_error != 0) { 214 | SetError("could not open font"); 215 | return; 216 | } 217 | 218 | if (num_faces == 0) num_faces = ft_face->num_faces; 219 | 220 | if (ft_face->family_name != nullptr) { 221 | protozero::pbf_writer fontstack_writer{pbf_writer, 1}; 222 | if (ft_face->style_name != nullptr) { 223 | fontstack_writer.add_string(1, std::string(ft_face->family_name) + " " + std::string(ft_face->style_name)); 224 | } else { 225 | fontstack_writer.add_string(1, std::string(ft_face->family_name)); 226 | } 227 | fontstack_writer.add_string(2, std::to_string(start_) + "-" + std::to_string(end_)); 228 | 229 | const double scale_factor = 1.0; 230 | 231 | // Set character sizes. 232 | double size = 24 * scale_factor; 233 | FT_Set_Char_Size(ft_face, 0, static_cast(size * (1 << 6)), 0, 0); 234 | 235 | for (std::vector::size_type x = 0; x != chars_.size(); ++x) { 236 | FT_ULong char_code = chars_[x]; 237 | sdf_glyph_foundry::glyph_info glyph; 238 | // Get FreeType face from face_ptr. 239 | FT_UInt char_index = FT_Get_Char_Index(ft_face, char_code); 240 | if (char_index == 0U) continue; 241 | 242 | glyph.glyph_index = char_index; 243 | sdf_glyph_foundry::RenderSDF(glyph, 24, 3, 0.25, ft_face); 244 | 245 | // Add glyph to fontstack. 246 | protozero::pbf_writer glyph_writer{fontstack_writer, 3}; 247 | 248 | // shortening conversion 249 | if (char_code > std::numeric_limits::max()) { 250 | SetError("Invalid value for char_code: too large"); 251 | return; 252 | } 253 | glyph_writer.add_uint32(1, static_cast(char_code)); 254 | 255 | if (glyph.width > 0) glyph_writer.add_bytes(2, glyph.bitmap); 256 | 257 | // direct type conversions, no need for checking or casting 258 | glyph_writer.add_uint32(3, glyph.width); 259 | glyph_writer.add_uint32(4, glyph.height); 260 | glyph_writer.add_sint32(5, glyph.left); 261 | 262 | // conversions requiring checks, for safety and correctness 263 | 264 | // double to int 265 | double top = static_cast(glyph.top) - glyph.ascender; 266 | if (top < std::numeric_limits::min() || top > std::numeric_limits::max()) { 267 | SetError("Invalid value for glyph.top-glyph.ascender"); 268 | return; 269 | } 270 | glyph_writer.add_sint32(6, static_cast(top)); 271 | 272 | // double to uint 273 | if (glyph.advance < std::numeric_limits::min() || glyph.advance > std::numeric_limits::max()) { 274 | SetError("Invalid value for glyph.top-glyph.ascender"); 275 | return; 276 | } 277 | glyph_writer.add_uint32(7, static_cast(glyph.advance)); 278 | } 279 | } else { 280 | SetError("font does not have family_name"); 281 | return; 282 | } 283 | } 284 | } catch (std::exception const& ex) { 285 | SetError(ex.what()); 286 | } 287 | } 288 | 289 | std::vector GetResult(Napi::Env env) override { 290 | return {env.Null(), Napi::Buffer::Copy(env, message_.data(), message_.size())}; 291 | } 292 | 293 | private: 294 | char const* font_data_; 295 | std::size_t font_size_; 296 | std::uint32_t start_; 297 | std::uint32_t end_; 298 | Napi::Reference> buffer_ref_; 299 | std::vector chars_; 300 | std::string message_; 301 | }; 302 | 303 | Napi::Value Load(Napi::CallbackInfo const& info) { 304 | Napi::Env env = info.Env(); 305 | // Validate arguments. 306 | if (info.Length() < 1 || !info[0].IsObject()) { 307 | Napi::TypeError::New(env, "First argument must be a font buffer").ThrowAsJavaScriptException(); 308 | return env.Undefined(); 309 | } 310 | Napi::Object obj = info[0].As(); 311 | 312 | if (!obj.IsBuffer()) { 313 | Napi::TypeError::New(env, "First argument must be a font buffer").ThrowAsJavaScriptException(); 314 | return env.Undefined(); 315 | } 316 | 317 | if (info.Length() < 2 || !info[1].IsFunction()) { 318 | Napi::TypeError::New(env, "Callback must be a function").ThrowAsJavaScriptException(); 319 | return env.Undefined(); 320 | } 321 | auto* worker = new AsyncLoad(obj.As>(), info[1].As()); 322 | worker->Queue(); 323 | return env.Undefined(); 324 | } 325 | 326 | Napi::Value Range(Napi::CallbackInfo const& info) { 327 | Napi::Env env = info.Env(); 328 | 329 | // Validate arguments. 330 | if (info.Length() < 1 || !info[0].IsObject()) { 331 | Napi::TypeError::New(env, "First argument must be an object of options").ThrowAsJavaScriptException(); 332 | return env.Undefined(); 333 | } 334 | Napi::Object options = info[0].As(); 335 | Napi::Value font_buffer = options.Get("font"); 336 | if (!font_buffer.IsObject()) { 337 | Napi::TypeError::New(env, "Font buffer is not an object").ThrowAsJavaScriptException(); 338 | return env.Undefined(); 339 | } 340 | Napi::Object obj = font_buffer.As(); 341 | Napi::Value start_val = options.Get("start"); 342 | Napi::Value end_val = options.Get("end"); 343 | 344 | if (!obj.IsBuffer()) { 345 | Napi::TypeError::New(env, "First argument must be a font buffer").ThrowAsJavaScriptException(); 346 | return env.Undefined(); 347 | } 348 | 349 | if (!start_val.IsNumber() || start_val.As().Int32Value() < 0) { 350 | Napi::TypeError::New(env, "option `start` must be a number from 0-65535").ThrowAsJavaScriptException(); 351 | return env.Undefined(); 352 | } 353 | 354 | if (!end_val.IsNumber() || end_val.As().Int32Value() > 65535) { 355 | Napi::TypeError::New(env, "option `end` must be a number from 0-65535").ThrowAsJavaScriptException(); 356 | return env.Undefined(); 357 | } 358 | std::uint32_t start = start_val.As().Uint32Value(); 359 | std::uint32_t end = end_val.As().Uint32Value(); 360 | 361 | if (end < start) { 362 | Napi::TypeError::New(env, "`start` must be less than or equal to `end`").ThrowAsJavaScriptException(); 363 | return env.Undefined(); 364 | } 365 | 366 | if (info.Length() < 2 || !info[1].IsFunction()) { 367 | Napi::TypeError::New(env, "Callback must be a function").ThrowAsJavaScriptException(); 368 | return env.Undefined(); 369 | } 370 | 371 | auto* worker = new AsyncRange(obj.As>(), start, end, info[1].As()); 372 | worker->Queue(); 373 | return env.Undefined(); 374 | } 375 | 376 | struct AsyncComposite : Napi::AsyncWorker { 377 | using Base = Napi::AsyncWorker; 378 | 379 | AsyncComposite(std::vector>&& glyphs, Napi::Function const& callback) 380 | : Base(callback), 381 | glyphs_(std::move(glyphs)), 382 | message_(std::make_unique()) {} 383 | 384 | void Execute() override { 385 | try { 386 | std::vector>> buffer_cache; 387 | std::map id_mapping; 388 | bool first_buffer = true; 389 | std::string fontstack_name; 390 | std::string range; 391 | protozero::pbf_writer pbf_writer(*message_); 392 | protozero::pbf_writer fontstack_writer{pbf_writer, 1}; 393 | // TODO(danespringmeyer): avoid duplicate fontstacks to be sent it 394 | for (auto const& glyph_obj : glyphs_) { 395 | protozero::data_view data_view{}; 396 | if (gzip::is_compressed(glyph_obj->data.data(), glyph_obj->data.size())) { 397 | buffer_cache.push_back(std::make_unique>()); 398 | gzip::Decompressor decompressor; 399 | decompressor.decompress(*buffer_cache.back(), glyph_obj->data.data(), glyph_obj->data.size()); 400 | data_view = protozero::data_view{buffer_cache.back()->data(), buffer_cache.back()->size()}; 401 | } else { 402 | data_view = glyph_obj->data; 403 | } 404 | protozero::pbf_reader fontstack_reader(data_view); 405 | while (fontstack_reader.next(1)) { 406 | auto stack_reader = fontstack_reader.get_message(); 407 | while (stack_reader.next()) { 408 | switch (stack_reader.tag()) { 409 | case 1: // name 410 | { 411 | if (first_buffer) { 412 | fontstack_name = stack_reader.get_string(); 413 | } else { 414 | fontstack_name = fontstack_name + ", " + stack_reader.get_string(); 415 | } 416 | break; 417 | } 418 | case 2: // range 419 | { 420 | if (first_buffer) { 421 | range = stack_reader.get_string(); 422 | } else { 423 | stack_reader.skip(); 424 | } 425 | break; 426 | } 427 | case 3: // glyphs 428 | { 429 | auto glyphs_data = stack_reader.get_view(); 430 | // collect all ids from first 431 | if (first_buffer) { 432 | protozero::pbf_reader glyphs_reader(glyphs_data); 433 | std::uint32_t glyph_id; 434 | while (glyphs_reader.next(1)) { 435 | glyph_id = glyphs_reader.get_uint32(); 436 | } 437 | id_mapping.emplace(glyph_id, glyphs_data); 438 | } else { 439 | protozero::pbf_reader glyphs_reader(glyphs_data); 440 | std::uint32_t glyph_id; 441 | while (glyphs_reader.next(1)) { 442 | glyph_id = glyphs_reader.get_uint32(); 443 | } 444 | auto search = id_mapping.find(glyph_id); 445 | if (search == id_mapping.end()) { 446 | id_mapping.emplace(glyph_id, glyphs_data); 447 | } 448 | } 449 | break; 450 | } 451 | default: 452 | // ignore data for unknown tags to allow for future extensions 453 | stack_reader.skip(); 454 | } 455 | } 456 | } 457 | first_buffer = false; 458 | } 459 | fontstack_writer.add_string(1, fontstack_name); 460 | fontstack_writer.add_string(2, range); 461 | for (auto const& glyph_pair : id_mapping) { 462 | fontstack_writer.add_message(3, glyph_pair.second); 463 | } 464 | } catch (std::exception const& ex) { 465 | SetError(ex.what()); 466 | } 467 | } 468 | 469 | std::vector GetResult(Napi::Env env) override { 470 | std::string& str = *message_; 471 | auto buffer = Napi::Buffer::New( 472 | env, &str[0], str.size(), 473 | [](Napi::Env env_, char* /*unused*/, std::string* str_ptr) { 474 | if (str_ptr != nullptr) { 475 | Napi::MemoryManagement::AdjustExternalMemory(env_, -static_cast(str_ptr->size())); 476 | } 477 | delete str_ptr; 478 | }, 479 | message_.release()); 480 | Napi::MemoryManagement::AdjustExternalMemory(env, static_cast(str.size())); 481 | return {env.Null(), buffer}; 482 | } 483 | 484 | private: 485 | std::vector> glyphs_; 486 | std::unique_ptr message_; 487 | }; 488 | 489 | Napi::Value Composite(const Napi::CallbackInfo& info) { 490 | Napi::Env env = info.Env(); 491 | 492 | // validate callback function 493 | Napi::Value callback_val = info[info.Length() - 1]; 494 | if (!callback_val.IsFunction()) { 495 | Napi::Error::New(env, "last argument must be a callback function").ThrowAsJavaScriptException(); 496 | return env.Undefined(); 497 | } 498 | // validate glyphPBF array 499 | if (!info[0].IsArray()) { 500 | Napi::TypeError::New(env, "first arg 'glyphs' must be an array of glyphs objects").ThrowAsJavaScriptException(); 501 | return env.Undefined(); 502 | } 503 | 504 | Napi::Array glyphs_array = info[0].As(); 505 | std::size_t num_glyphs = glyphs_array.Length(); 506 | 507 | if (num_glyphs <= 0) { 508 | Napi::TypeError::New(env, "'glyphs' array must be of length greater than 0").ThrowAsJavaScriptException(); 509 | return env.Undefined(); 510 | } 511 | 512 | std::vector> glyphs{}; 513 | glyphs.reserve(num_glyphs); 514 | for (std::uint32_t index = 0; index < num_glyphs; ++index) { 515 | Napi::Value buf_val = glyphs_array.Get(index); 516 | if (!buf_val.IsBuffer()) { 517 | Napi::TypeError::New(env, "buffer value in 'glyphs' array item is not a true buffer"); 518 | return env.Undefined(); 519 | } 520 | Napi::Buffer buffer = buf_val.As>(); 521 | if (buffer.IsEmpty()) { 522 | Napi::TypeError::New(env, "buffer value in 'glyphs' array is empty"); 523 | return env.Undefined(); 524 | } 525 | glyphs.push_back(std::make_unique(buffer)); 526 | } 527 | 528 | auto* worker = new AsyncComposite(std::move(glyphs), callback_val.As()); 529 | worker->Queue(); 530 | return env.Undefined(); 531 | } 532 | 533 | } // namespace node_fontnik 534 | -------------------------------------------------------------------------------- /test/expected/range.json: -------------------------------------------------------------------------------- 1 | { 2 | "stacks": { 3 | "Open Sans Regular": { 4 | "glyphs": { 5 | "32": { 6 | "id": 32, 7 | "width": 0, 8 | "height": 0, 9 | "left": 0, 10 | "top": -26, 11 | "advance": 6 12 | }, 13 | "33": { 14 | "id": 33, 15 | "width": 3, 16 | "height": 17, 17 | "left": 2, 18 | "top": -9, 19 | "advance": 6 20 | }, 21 | "34": { 22 | "id": 34, 23 | "width": 6, 24 | "height": 6, 25 | "left": 2, 26 | "top": -9, 27 | "advance": 9 28 | }, 29 | "35": { 30 | "id": 35, 31 | "width": 14, 32 | "height": 17, 33 | "left": 1, 34 | "top": -9, 35 | "advance": 15 36 | }, 37 | "36": { 38 | "id": 36, 39 | "width": 10, 40 | "height": 19, 41 | "left": 2, 42 | "top": -8, 43 | "advance": 13 44 | }, 45 | "37": { 46 | "id": 37, 47 | "width": 18, 48 | "height": 17, 49 | "left": 1, 50 | "top": -9, 51 | "advance": 19 52 | }, 53 | "38": { 54 | "id": 38, 55 | "width": 16, 56 | "height": 17, 57 | "left": 1, 58 | "top": -9, 59 | "advance": 17 60 | }, 61 | "39": { 62 | "id": 39, 63 | "width": 2, 64 | "height": 6, 65 | "left": 2, 66 | "top": -9, 67 | "advance": 5 68 | }, 69 | "40": { 70 | "id": 40, 71 | "width": 5, 72 | "height": 21, 73 | "left": 1, 74 | "top": -9, 75 | "advance": 7 76 | }, 77 | "41": { 78 | "id": 41, 79 | "width": 5, 80 | "height": 21, 81 | "left": 1, 82 | "top": -9, 83 | "advance": 7 84 | }, 85 | "42": { 86 | "id": 42, 87 | "width": 11, 88 | "height": 11, 89 | "left": 1, 90 | "top": -8, 91 | "advance": 13 92 | }, 93 | "43": { 94 | "id": 43, 95 | "width": 11, 96 | "height": 11, 97 | "left": 1, 98 | "top": -12, 99 | "advance": 13 100 | }, 101 | "44": { 102 | "id": 44, 103 | "width": 3, 104 | "height": 6, 105 | "left": 1, 106 | "top": -23, 107 | "advance": 5 108 | }, 109 | "45": { 110 | "id": 45, 111 | "width": 6, 112 | "height": 1, 113 | "left": 1, 114 | "top": -19, 115 | "advance": 7 116 | }, 117 | "46": { 118 | "id": 46, 119 | "width": 3, 120 | "height": 3, 121 | "left": 2, 122 | "top": -23, 123 | "advance": 6 124 | }, 125 | "47": { 126 | "id": 47, 127 | "width": 9, 128 | "height": 17, 129 | "left": 0, 130 | "top": -9, 131 | "advance": 8 132 | }, 133 | "48": { 134 | "id": 48, 135 | "width": 12, 136 | "height": 17, 137 | "left": 1, 138 | "top": -9, 139 | "advance": 13 140 | }, 141 | "49": { 142 | "id": 49, 143 | "width": 6, 144 | "height": 17, 145 | "left": 2, 146 | "top": -9, 147 | "advance": 13 148 | }, 149 | "50": { 150 | "id": 50, 151 | "width": 11, 152 | "height": 17, 153 | "left": 1, 154 | "top": -9, 155 | "advance": 13 156 | }, 157 | "51": { 158 | "id": 51, 159 | "width": 11, 160 | "height": 17, 161 | "left": 1, 162 | "top": -9, 163 | "advance": 13 164 | }, 165 | "52": { 166 | "id": 52, 167 | "width": 12, 168 | "height": 17, 169 | "left": 1, 170 | "top": -9, 171 | "advance": 13 172 | }, 173 | "53": { 174 | "id": 53, 175 | "width": 10, 176 | "height": 17, 177 | "left": 2, 178 | "top": -9, 179 | "advance": 13 180 | }, 181 | "54": { 182 | "id": 54, 183 | "width": 12, 184 | "height": 17, 185 | "left": 1, 186 | "top": -9, 187 | "advance": 13 188 | }, 189 | "55": { 190 | "id": 55, 191 | "width": 12, 192 | "height": 17, 193 | "left": 1, 194 | "top": -9, 195 | "advance": 13 196 | }, 197 | "56": { 198 | "id": 56, 199 | "width": 11, 200 | "height": 17, 201 | "left": 1, 202 | "top": -9, 203 | "advance": 13 204 | }, 205 | "57": { 206 | "id": 57, 207 | "width": 11, 208 | "height": 17, 209 | "left": 1, 210 | "top": -9, 211 | "advance": 13 212 | }, 213 | "58": { 214 | "id": 58, 215 | "width": 3, 216 | "height": 13, 217 | "left": 2, 218 | "top": -13, 219 | "advance": 6 220 | }, 221 | "59": { 222 | "id": 59, 223 | "width": 4, 224 | "height": 16, 225 | "left": 1, 226 | "top": -13, 227 | "advance": 6 228 | }, 229 | "60": { 230 | "id": 60, 231 | "width": 11, 232 | "height": 12, 233 | "left": 1, 234 | "top": -11, 235 | "advance": 13 236 | }, 237 | "61": { 238 | "id": 61, 239 | "width": 11, 240 | "height": 7, 241 | "left": 1, 242 | "top": -14, 243 | "advance": 13 244 | }, 245 | "62": { 246 | "id": 62, 247 | "width": 11, 248 | "height": 12, 249 | "left": 1, 250 | "top": -11, 251 | "advance": 13 252 | }, 253 | "63": { 254 | "id": 63, 255 | "width": 10, 256 | "height": 17, 257 | "left": 0, 258 | "top": -9, 259 | "advance": 10 260 | }, 261 | "64": { 262 | "id": 64, 263 | "width": 19, 264 | "height": 19, 265 | "left": 1, 266 | "top": -9, 267 | "advance": 21 268 | }, 269 | "65": { 270 | "id": 65, 271 | "width": 15, 272 | "height": 17, 273 | "left": 0, 274 | "top": -9, 275 | "advance": 15 276 | }, 277 | "66": { 278 | "id": 66, 279 | "width": 12, 280 | "height": 17, 281 | "left": 2, 282 | "top": -9, 283 | "advance": 15 284 | }, 285 | "67": { 286 | "id": 67, 287 | "width": 13, 288 | "height": 17, 289 | "left": 1, 290 | "top": -9, 291 | "advance": 15 292 | }, 293 | "68": { 294 | "id": 68, 295 | "width": 14, 296 | "height": 17, 297 | "left": 2, 298 | "top": -9, 299 | "advance": 17 300 | }, 301 | "69": { 302 | "id": 69, 303 | "width": 10, 304 | "height": 17, 305 | "left": 2, 306 | "top": -9, 307 | "advance": 13 308 | }, 309 | "70": { 310 | "id": 70, 311 | "width": 10, 312 | "height": 17, 313 | "left": 2, 314 | "top": -9, 315 | "advance": 12 316 | }, 317 | "71": { 318 | "id": 71, 319 | "width": 15, 320 | "height": 17, 321 | "left": 1, 322 | "top": -9, 323 | "advance": 17 324 | }, 325 | "72": { 326 | "id": 72, 327 | "width": 13, 328 | "height": 17, 329 | "left": 2, 330 | "top": -9, 331 | "advance": 17 332 | }, 333 | "73": { 334 | "id": 73, 335 | "width": 2, 336 | "height": 17, 337 | "left": 2, 338 | "top": -9, 339 | "advance": 6 340 | }, 341 | "74": { 342 | "id": 74, 343 | "width": 6, 344 | "height": 22, 345 | "left": -2, 346 | "top": -9, 347 | "advance": 6 348 | }, 349 | "75": { 350 | "id": 75, 351 | "width": 13, 352 | "height": 17, 353 | "left": 2, 354 | "top": -9, 355 | "advance": 14 356 | }, 357 | "76": { 358 | "id": 76, 359 | "width": 10, 360 | "height": 17, 361 | "left": 2, 362 | "top": -9, 363 | "advance": 12 364 | }, 365 | "77": { 366 | "id": 77, 367 | "width": 17, 368 | "height": 17, 369 | "left": 2, 370 | "top": -9, 371 | "advance": 21 372 | }, 373 | "78": { 374 | "id": 78, 375 | "width": 14, 376 | "height": 17, 377 | "left": 2, 378 | "top": -9, 379 | "advance": 18 380 | }, 381 | "79": { 382 | "id": 79, 383 | "width": 16, 384 | "height": 17, 385 | "left": 1, 386 | "top": -9, 387 | "advance": 18 388 | }, 389 | "80": { 390 | "id": 80, 391 | "width": 11, 392 | "height": 17, 393 | "left": 2, 394 | "top": -9, 395 | "advance": 14 396 | }, 397 | "81": { 398 | "id": 81, 399 | "width": 16, 400 | "height": 21, 401 | "left": 1, 402 | "top": -9, 403 | "advance": 18 404 | }, 405 | "82": { 406 | "id": 82, 407 | "width": 12, 408 | "height": 17, 409 | "left": 2, 410 | "top": -9, 411 | "advance": 14 412 | }, 413 | "83": { 414 | "id": 83, 415 | "width": 11, 416 | "height": 17, 417 | "left": 1, 418 | "top": -9, 419 | "advance": 13 420 | }, 421 | "84": { 422 | "id": 84, 423 | "width": 13, 424 | "height": 17, 425 | "left": 0, 426 | "top": -9, 427 | "advance": 13 428 | }, 429 | "85": { 430 | "id": 85, 431 | "width": 13, 432 | "height": 17, 433 | "left": 2, 434 | "top": -9, 435 | "advance": 17 436 | }, 437 | "86": { 438 | "id": 86, 439 | "width": 14, 440 | "height": 17, 441 | "left": 0, 442 | "top": -9, 443 | "advance": 14 444 | }, 445 | "87": { 446 | "id": 87, 447 | "width": 22, 448 | "height": 17, 449 | "left": 0, 450 | "top": -9, 451 | "advance": 22 452 | }, 453 | "88": { 454 | "id": 88, 455 | "width": 14, 456 | "height": 17, 457 | "left": 0, 458 | "top": -9, 459 | "advance": 13 460 | }, 461 | "89": { 462 | "id": 89, 463 | "width": 13, 464 | "height": 17, 465 | "left": 0, 466 | "top": -9, 467 | "advance": 13 468 | }, 469 | "90": { 470 | "id": 90, 471 | "width": 12, 472 | "height": 17, 473 | "left": 1, 474 | "top": -9, 475 | "advance": 13 476 | }, 477 | "91": { 478 | "id": 91, 479 | "width": 5, 480 | "height": 21, 481 | "left": 2, 482 | "top": -9, 483 | "advance": 7 484 | }, 485 | "92": { 486 | "id": 92, 487 | "width": 9, 488 | "height": 17, 489 | "left": 0, 490 | "top": -9, 491 | "advance": 8 492 | }, 493 | "93": { 494 | "id": 93, 495 | "width": 5, 496 | "height": 21, 497 | "left": 1, 498 | "top": -9, 499 | "advance": 7 500 | }, 501 | "94": { 502 | "id": 94, 503 | "width": 11, 504 | "height": 11, 505 | "left": 1, 506 | "top": -9, 507 | "advance": 13 508 | }, 509 | "95": { 510 | "id": 95, 511 | "width": 11, 512 | "height": 2, 513 | "left": 0, 514 | "top": -28, 515 | "advance": 10 516 | }, 517 | "96": { 518 | "id": 96, 519 | "width": 4, 520 | "height": 3, 521 | "left": 5, 522 | "top": -8, 523 | "advance": 13 524 | }, 525 | "97": { 526 | "id": 97, 527 | "width": 10, 528 | "height": 13, 529 | "left": 1, 530 | "top": -13, 531 | "advance": 13 532 | }, 533 | "98": { 534 | "id": 98, 535 | "width": 11, 536 | "height": 18, 537 | "left": 2, 538 | "top": -8, 539 | "advance": 14 540 | }, 541 | "99": { 542 | "id": 99, 543 | "width": 10, 544 | "height": 13, 545 | "left": 1, 546 | "top": -13, 547 | "advance": 11 548 | }, 549 | "100": { 550 | "id": 100, 551 | "width": 12, 552 | "height": 18, 553 | "left": 1, 554 | "top": -8, 555 | "advance": 14 556 | }, 557 | "101": { 558 | "id": 101, 559 | "width": 11, 560 | "height": 13, 561 | "left": 1, 562 | "top": -13, 563 | "advance": 13 564 | }, 565 | "102": { 566 | "id": 102, 567 | "width": 9, 568 | "height": 18, 569 | "left": 0, 570 | "top": -8, 571 | "advance": 8 572 | }, 573 | "103": { 574 | "id": 103, 575 | "width": 13, 576 | "height": 19, 577 | "left": 0, 578 | "top": -13, 579 | "advance": 13 580 | }, 581 | "104": { 582 | "id": 104, 583 | "width": 11, 584 | "height": 18, 585 | "left": 2, 586 | "top": -8, 587 | "advance": 14 588 | }, 589 | "105": { 590 | "id": 105, 591 | "width": 2, 592 | "height": 18, 593 | "left": 2, 594 | "top": -8, 595 | "advance": 6 596 | }, 597 | "106": { 598 | "id": 106, 599 | "width": 5, 600 | "height": 24, 601 | "left": -1, 602 | "top": -8, 603 | "advance": 6 604 | }, 605 | "107": { 606 | "id": 107, 607 | "width": 10, 608 | "height": 18, 609 | "left": 2, 610 | "top": -8, 611 | "advance": 12 612 | }, 613 | "108": { 614 | "id": 108, 615 | "width": 2, 616 | "height": 18, 617 | "left": 2, 618 | "top": -8, 619 | "advance": 6 620 | }, 621 | "109": { 622 | "id": 109, 623 | "width": 18, 624 | "height": 13, 625 | "left": 2, 626 | "top": -13, 627 | "advance": 22 628 | }, 629 | "110": { 630 | "id": 110, 631 | "width": 11, 632 | "height": 13, 633 | "left": 2, 634 | "top": -13, 635 | "advance": 14 636 | }, 637 | "111": { 638 | "id": 111, 639 | "width": 12, 640 | "height": 13, 641 | "left": 1, 642 | "top": -13, 643 | "advance": 14 644 | }, 645 | "112": { 646 | "id": 112, 647 | "width": 11, 648 | "height": 19, 649 | "left": 2, 650 | "top": -13, 651 | "advance": 14 652 | }, 653 | "113": { 654 | "id": 113, 655 | "width": 12, 656 | "height": 19, 657 | "left": 1, 658 | "top": -13, 659 | "advance": 14 660 | }, 661 | "114": { 662 | "id": 114, 663 | "width": 7, 664 | "height": 13, 665 | "left": 2, 666 | "top": -13, 667 | "advance": 9 668 | }, 669 | "115": { 670 | "id": 115, 671 | "width": 9, 672 | "height": 13, 673 | "left": 1, 674 | "top": -13, 675 | "advance": 11 676 | }, 677 | "116": { 678 | "id": 116, 679 | "width": 8, 680 | "height": 16, 681 | "left": 0, 682 | "top": -10, 683 | "advance": 8 684 | }, 685 | "117": { 686 | "id": 117, 687 | "width": 11, 688 | "height": 13, 689 | "left": 2, 690 | "top": -13, 691 | "advance": 14 692 | }, 693 | "118": { 694 | "id": 118, 695 | "width": 12, 696 | "height": 13, 697 | "left": 0, 698 | "top": -13, 699 | "advance": 12 700 | }, 701 | "119": { 702 | "id": 119, 703 | "width": 18, 704 | "height": 13, 705 | "left": 0, 706 | "top": -13, 707 | "advance": 18 708 | }, 709 | "120": { 710 | "id": 120, 711 | "width": 12, 712 | "height": 13, 713 | "left": 0, 714 | "top": -13, 715 | "advance": 12 716 | }, 717 | "121": { 718 | "id": 121, 719 | "width": 12, 720 | "height": 19, 721 | "left": 0, 722 | "top": -13, 723 | "advance": 12 724 | }, 725 | "122": { 726 | "id": 122, 727 | "width": 9, 728 | "height": 13, 729 | "left": 1, 730 | "top": -13, 731 | "advance": 11 732 | }, 733 | "123": { 734 | "id": 123, 735 | "width": 7, 736 | "height": 21, 737 | "left": 1, 738 | "top": -9, 739 | "advance": 9 740 | }, 741 | "124": { 742 | "id": 124, 743 | "width": 1, 744 | "height": 24, 745 | "left": 6, 746 | "top": -8, 747 | "advance": 13 748 | }, 749 | "125": { 750 | "id": 125, 751 | "width": 7, 752 | "height": 21, 753 | "left": 1, 754 | "top": -9, 755 | "advance": 9 756 | }, 757 | "126": { 758 | "id": 126, 759 | "width": 11, 760 | "height": 3, 761 | "left": 1, 762 | "top": -16, 763 | "advance": 13 764 | }, 765 | "160": { 766 | "id": 160, 767 | "width": 0, 768 | "height": 0, 769 | "left": 0, 770 | "top": -26, 771 | "advance": 6 772 | }, 773 | "161": { 774 | "id": 161, 775 | "width": 3, 776 | "height": 17, 777 | "left": 2, 778 | "top": -13, 779 | "advance": 6 780 | }, 781 | "162": { 782 | "id": 162, 783 | "width": 10, 784 | "height": 17, 785 | "left": 2, 786 | "top": -9, 787 | "advance": 13 788 | }, 789 | "163": { 790 | "id": 163, 791 | "width": 12, 792 | "height": 17, 793 | "left": 1, 794 | "top": -9, 795 | "advance": 13 796 | }, 797 | "164": { 798 | "id": 164, 799 | "width": 11, 800 | "height": 11, 801 | "left": 1, 802 | "top": -12, 803 | "advance": 13 804 | }, 805 | "165": { 806 | "id": 165, 807 | "width": 13, 808 | "height": 17, 809 | "left": 0, 810 | "top": -9, 811 | "advance": 13 812 | }, 813 | "166": { 814 | "id": 166, 815 | "width": 1, 816 | "height": 24, 817 | "left": 6, 818 | "top": -8, 819 | "advance": 13 820 | }, 821 | "167": { 822 | "id": 167, 823 | "width": 10, 824 | "height": 18, 825 | "left": 1, 826 | "top": -8, 827 | "advance": 12 828 | }, 829 | "168": { 830 | "id": 168, 831 | "width": 6, 832 | "height": 2, 833 | "left": 4, 834 | "top": -9, 835 | "advance": 13 836 | }, 837 | "169": { 838 | "id": 169, 839 | "width": 18, 840 | "height": 17, 841 | "left": 1, 842 | "top": -9, 843 | "advance": 19 844 | }, 845 | "170": { 846 | "id": 170, 847 | "width": 6, 848 | "height": 8, 849 | "left": 1, 850 | "top": -9, 851 | "advance": 8 852 | }, 853 | "171": { 854 | "id": 171, 855 | "width": 10, 856 | "height": 10, 857 | "left": 1, 858 | "top": -15, 859 | "advance": 11 860 | }, 861 | "172": { 862 | "id": 172, 863 | "width": 11, 864 | "height": 6, 865 | "left": 1, 866 | "top": -17, 867 | "advance": 13 868 | }, 869 | "173": { 870 | "id": 173, 871 | "width": 6, 872 | "height": 1, 873 | "left": 1, 874 | "top": -19, 875 | "advance": 7 876 | }, 877 | "174": { 878 | "id": 174, 879 | "width": 18, 880 | "height": 17, 881 | "left": 1, 882 | "top": -9, 883 | "advance": 19 884 | }, 885 | "175": { 886 | "id": 175, 887 | "width": 12, 888 | "height": 2, 889 | "left": 0, 890 | "top": -6, 891 | "advance": 12 892 | }, 893 | "176": { 894 | "id": 176, 895 | "width": 8, 896 | "height": 7, 897 | "left": 1, 898 | "top": -9, 899 | "advance": 10 900 | }, 901 | "177": { 902 | "id": 177, 903 | "width": 11, 904 | "height": 14, 905 | "left": 1, 906 | "top": -12, 907 | "advance": 13 908 | }, 909 | "178": { 910 | "id": 178, 911 | "width": 7, 912 | "height": 10, 913 | "left": 1, 914 | "top": -9, 915 | "advance": 8 916 | }, 917 | "179": { 918 | "id": 179, 919 | "width": 8, 920 | "height": 10, 921 | "left": 0, 922 | "top": -9, 923 | "advance": 8 924 | }, 925 | "180": { 926 | "id": 180, 927 | "width": 4, 928 | "height": 3, 929 | "left": 5, 930 | "top": -8, 931 | "advance": 13 932 | }, 933 | "181": { 934 | "id": 181, 935 | "width": 11, 936 | "height": 19, 937 | "left": 2, 938 | "top": -13, 939 | "advance": 14 940 | }, 941 | "182": { 942 | "id": 182, 943 | "width": 12, 944 | "height": 21, 945 | "left": 1, 946 | "top": -8, 947 | "advance": 15 948 | }, 949 | "183": { 950 | "id": 183, 951 | "width": 3, 952 | "height": 3, 953 | "left": 2, 954 | "top": -16, 955 | "advance": 6 956 | }, 957 | "184": { 958 | "id": 184, 959 | "width": 5, 960 | "height": 6, 961 | "left": 0, 962 | "top": -26, 963 | "advance": 5 964 | }, 965 | "185": { 966 | "id": 185, 967 | "width": 5, 968 | "height": 10, 969 | "left": 1, 970 | "top": -9, 971 | "advance": 8 972 | }, 973 | "186": { 974 | "id": 186, 975 | "width": 7, 976 | "height": 8, 977 | "left": 1, 978 | "top": -9, 979 | "advance": 9 980 | }, 981 | "187": { 982 | "id": 187, 983 | "width": 10, 984 | "height": 10, 985 | "left": 1, 986 | "top": -15, 987 | "advance": 11 988 | }, 989 | "188": { 990 | "id": 188, 991 | "width": 16, 992 | "height": 17, 993 | "left": 1, 994 | "top": -9, 995 | "advance": 18 996 | }, 997 | "189": { 998 | "id": 189, 999 | "width": 17, 1000 | "height": 17, 1001 | "left": 1, 1002 | "top": -9, 1003 | "advance": 18 1004 | }, 1005 | "190": { 1006 | "id": 190, 1007 | "width": 18, 1008 | "height": 17, 1009 | "left": 0, 1010 | "top": -9, 1011 | "advance": 18 1012 | }, 1013 | "191": { 1014 | "id": 191, 1015 | "width": 9, 1016 | "height": 18, 1017 | "left": 1, 1018 | "top": -13, 1019 | "advance": 10 1020 | }, 1021 | "192": { 1022 | "id": 192, 1023 | "width": 15, 1024 | "height": 22, 1025 | "left": 0, 1026 | "top": -4, 1027 | "advance": 15 1028 | }, 1029 | "193": { 1030 | "id": 193, 1031 | "width": 15, 1032 | "height": 22, 1033 | "left": 0, 1034 | "top": -4, 1035 | "advance": 15 1036 | }, 1037 | "194": { 1038 | "id": 194, 1039 | "width": 15, 1040 | "height": 22, 1041 | "left": 0, 1042 | "top": -4, 1043 | "advance": 15 1044 | }, 1045 | "195": { 1046 | "id": 195, 1047 | "width": 15, 1048 | "height": 22, 1049 | "left": 0, 1050 | "top": -4, 1051 | "advance": 15 1052 | }, 1053 | "196": { 1054 | "id": 196, 1055 | "width": 15, 1056 | "height": 21, 1057 | "left": 0, 1058 | "top": -5, 1059 | "advance": 15 1060 | }, 1061 | "197": { 1062 | "id": 197, 1063 | "width": 15, 1064 | "height": 22, 1065 | "left": 0, 1066 | "top": -4, 1067 | "advance": 15 1068 | }, 1069 | "198": { 1070 | "id": 198, 1071 | "width": 20, 1072 | "height": 17, 1073 | "left": 0, 1074 | "top": -9, 1075 | "advance": 20 1076 | }, 1077 | "199": { 1078 | "id": 199, 1079 | "width": 13, 1080 | "height": 23, 1081 | "left": 1, 1082 | "top": -9, 1083 | "advance": 15 1084 | }, 1085 | "200": { 1086 | "id": 200, 1087 | "width": 10, 1088 | "height": 22, 1089 | "left": 2, 1090 | "top": -4, 1091 | "advance": 13 1092 | }, 1093 | "201": { 1094 | "id": 201, 1095 | "width": 10, 1096 | "height": 22, 1097 | "left": 2, 1098 | "top": -4, 1099 | "advance": 13 1100 | }, 1101 | "202": { 1102 | "id": 202, 1103 | "width": 10, 1104 | "height": 22, 1105 | "left": 2, 1106 | "top": -4, 1107 | "advance": 13 1108 | }, 1109 | "203": { 1110 | "id": 203, 1111 | "width": 10, 1112 | "height": 21, 1113 | "left": 2, 1114 | "top": -5, 1115 | "advance": 13 1116 | }, 1117 | "204": { 1118 | "id": 204, 1119 | "width": 4, 1120 | "height": 22, 1121 | "left": 0, 1122 | "top": -4, 1123 | "advance": 6 1124 | }, 1125 | "205": { 1126 | "id": 205, 1127 | "width": 4, 1128 | "height": 22, 1129 | "left": 2, 1130 | "top": -4, 1131 | "advance": 6 1132 | }, 1133 | "206": { 1134 | "id": 206, 1135 | "width": 8, 1136 | "height": 22, 1137 | "left": -1, 1138 | "top": -4, 1139 | "advance": 6 1140 | }, 1141 | "207": { 1142 | "id": 207, 1143 | "width": 6, 1144 | "height": 21, 1145 | "left": 0, 1146 | "top": -5, 1147 | "advance": 6 1148 | }, 1149 | "208": { 1150 | "id": 208, 1151 | "width": 15, 1152 | "height": 17, 1153 | "left": 1, 1154 | "top": -9, 1155 | "advance": 17 1156 | }, 1157 | "209": { 1158 | "id": 209, 1159 | "width": 14, 1160 | "height": 22, 1161 | "left": 2, 1162 | "top": -4, 1163 | "advance": 18 1164 | }, 1165 | "210": { 1166 | "id": 210, 1167 | "width": 16, 1168 | "height": 22, 1169 | "left": 1, 1170 | "top": -4, 1171 | "advance": 18 1172 | }, 1173 | "211": { 1174 | "id": 211, 1175 | "width": 16, 1176 | "height": 22, 1177 | "left": 1, 1178 | "top": -4, 1179 | "advance": 18 1180 | }, 1181 | "212": { 1182 | "id": 212, 1183 | "width": 16, 1184 | "height": 22, 1185 | "left": 1, 1186 | "top": -4, 1187 | "advance": 18 1188 | }, 1189 | "213": { 1190 | "id": 213, 1191 | "width": 16, 1192 | "height": 22, 1193 | "left": 1, 1194 | "top": -4, 1195 | "advance": 18 1196 | }, 1197 | "214": { 1198 | "id": 214, 1199 | "width": 16, 1200 | "height": 21, 1201 | "left": 1, 1202 | "top": -5, 1203 | "advance": 18 1204 | }, 1205 | "215": { 1206 | "id": 215, 1207 | "width": 10, 1208 | "height": 11, 1209 | "left": 2, 1210 | "top": -12, 1211 | "advance": 13 1212 | }, 1213 | "216": { 1214 | "id": 216, 1215 | "width": 16, 1216 | "height": 19, 1217 | "left": 1, 1218 | "top": -8, 1219 | "advance": 18 1220 | }, 1221 | "217": { 1222 | "id": 217, 1223 | "width": 13, 1224 | "height": 22, 1225 | "left": 2, 1226 | "top": -4, 1227 | "advance": 17 1228 | }, 1229 | "218": { 1230 | "id": 218, 1231 | "width": 13, 1232 | "height": 22, 1233 | "left": 2, 1234 | "top": -4, 1235 | "advance": 17 1236 | }, 1237 | "219": { 1238 | "id": 219, 1239 | "width": 13, 1240 | "height": 22, 1241 | "left": 2, 1242 | "top": -4, 1243 | "advance": 17 1244 | }, 1245 | "220": { 1246 | "id": 220, 1247 | "width": 13, 1248 | "height": 21, 1249 | "left": 2, 1250 | "top": -5, 1251 | "advance": 17 1252 | }, 1253 | "221": { 1254 | "id": 221, 1255 | "width": 13, 1256 | "height": 22, 1257 | "left": 0, 1258 | "top": -4, 1259 | "advance": 13 1260 | }, 1261 | "222": { 1262 | "id": 222, 1263 | "width": 11, 1264 | "height": 17, 1265 | "left": 2, 1266 | "top": -9, 1267 | "advance": 14 1268 | }, 1269 | "223": { 1270 | "id": 223, 1271 | "width": 12, 1272 | "height": 18, 1273 | "left": 2, 1274 | "top": -8, 1275 | "advance": 14 1276 | }, 1277 | "224": { 1278 | "id": 224, 1279 | "width": 10, 1280 | "height": 18, 1281 | "left": 1, 1282 | "top": -8, 1283 | "advance": 13 1284 | }, 1285 | "225": { 1286 | "id": 225, 1287 | "width": 10, 1288 | "height": 18, 1289 | "left": 1, 1290 | "top": -8, 1291 | "advance": 13 1292 | }, 1293 | "226": { 1294 | "id": 226, 1295 | "width": 10, 1296 | "height": 18, 1297 | "left": 1, 1298 | "top": -8, 1299 | "advance": 13 1300 | }, 1301 | "227": { 1302 | "id": 227, 1303 | "width": 10, 1304 | "height": 18, 1305 | "left": 1, 1306 | "top": -8, 1307 | "advance": 13 1308 | }, 1309 | "228": { 1310 | "id": 228, 1311 | "width": 10, 1312 | "height": 17, 1313 | "left": 1, 1314 | "top": -9, 1315 | "advance": 13 1316 | }, 1317 | "229": { 1318 | "id": 229, 1319 | "width": 10, 1320 | "height": 20, 1321 | "left": 1, 1322 | "top": -6, 1323 | "advance": 13 1324 | }, 1325 | "230": { 1326 | "id": 230, 1327 | "width": 18, 1328 | "height": 13, 1329 | "left": 1, 1330 | "top": -13, 1331 | "advance": 20 1332 | }, 1333 | "231": { 1334 | "id": 231, 1335 | "width": 10, 1336 | "height": 19, 1337 | "left": 1, 1338 | "top": -13, 1339 | "advance": 11 1340 | }, 1341 | "232": { 1342 | "id": 232, 1343 | "width": 11, 1344 | "height": 18, 1345 | "left": 1, 1346 | "top": -8, 1347 | "advance": 13 1348 | }, 1349 | "233": { 1350 | "id": 233, 1351 | "width": 11, 1352 | "height": 18, 1353 | "left": 1, 1354 | "top": -8, 1355 | "advance": 13 1356 | }, 1357 | "234": { 1358 | "id": 234, 1359 | "width": 11, 1360 | "height": 18, 1361 | "left": 1, 1362 | "top": -8, 1363 | "advance": 13 1364 | }, 1365 | "235": { 1366 | "id": 235, 1367 | "width": 11, 1368 | "height": 17, 1369 | "left": 1, 1370 | "top": -9, 1371 | "advance": 13 1372 | }, 1373 | "236": { 1374 | "id": 236, 1375 | "width": 4, 1376 | "height": 18, 1377 | "left": 0, 1378 | "top": -8, 1379 | "advance": 6 1380 | }, 1381 | "237": { 1382 | "id": 237, 1383 | "width": 4, 1384 | "height": 18, 1385 | "left": 2, 1386 | "top": -8, 1387 | "advance": 6 1388 | }, 1389 | "238": { 1390 | "id": 238, 1391 | "width": 8, 1392 | "height": 18, 1393 | "left": -1, 1394 | "top": -8, 1395 | "advance": 6 1396 | }, 1397 | "239": { 1398 | "id": 239, 1399 | "width": 6, 1400 | "height": 17, 1401 | "left": 0, 1402 | "top": -9, 1403 | "advance": 6 1404 | }, 1405 | "240": { 1406 | "id": 240, 1407 | "width": 12, 1408 | "height": 18, 1409 | "left": 1, 1410 | "top": -8, 1411 | "advance": 14 1412 | }, 1413 | "241": { 1414 | "id": 241, 1415 | "width": 11, 1416 | "height": 18, 1417 | "left": 2, 1418 | "top": -8, 1419 | "advance": 14 1420 | }, 1421 | "242": { 1422 | "id": 242, 1423 | "width": 12, 1424 | "height": 18, 1425 | "left": 1, 1426 | "top": -8, 1427 | "advance": 14 1428 | }, 1429 | "243": { 1430 | "id": 243, 1431 | "width": 12, 1432 | "height": 18, 1433 | "left": 1, 1434 | "top": -8, 1435 | "advance": 14 1436 | }, 1437 | "244": { 1438 | "id": 244, 1439 | "width": 12, 1440 | "height": 18, 1441 | "left": 1, 1442 | "top": -8, 1443 | "advance": 14 1444 | }, 1445 | "245": { 1446 | "id": 245, 1447 | "width": 12, 1448 | "height": 18, 1449 | "left": 1, 1450 | "top": -8, 1451 | "advance": 14 1452 | }, 1453 | "246": { 1454 | "id": 246, 1455 | "width": 12, 1456 | "height": 17, 1457 | "left": 1, 1458 | "top": -9, 1459 | "advance": 14 1460 | }, 1461 | "247": { 1462 | "id": 247, 1463 | "width": 11, 1464 | "height": 11, 1465 | "left": 1, 1466 | "top": -12, 1467 | "advance": 13 1468 | }, 1469 | "248": { 1470 | "id": 248, 1471 | "width": 12, 1472 | "height": 15, 1473 | "left": 1, 1474 | "top": -12, 1475 | "advance": 14 1476 | }, 1477 | "249": { 1478 | "id": 249, 1479 | "width": 11, 1480 | "height": 18, 1481 | "left": 2, 1482 | "top": -8, 1483 | "advance": 14 1484 | }, 1485 | "250": { 1486 | "id": 250, 1487 | "width": 11, 1488 | "height": 18, 1489 | "left": 2, 1490 | "top": -8, 1491 | "advance": 14 1492 | }, 1493 | "251": { 1494 | "id": 251, 1495 | "width": 11, 1496 | "height": 18, 1497 | "left": 2, 1498 | "top": -8, 1499 | "advance": 14 1500 | }, 1501 | "252": { 1502 | "id": 252, 1503 | "width": 11, 1504 | "height": 17, 1505 | "left": 2, 1506 | "top": -9, 1507 | "advance": 14 1508 | }, 1509 | "253": { 1510 | "id": 253, 1511 | "width": 12, 1512 | "height": 24, 1513 | "left": 0, 1514 | "top": -8, 1515 | "advance": 12 1516 | }, 1517 | "254": { 1518 | "id": 254, 1519 | "width": 11, 1520 | "height": 24, 1521 | "left": 2, 1522 | "top": -8, 1523 | "advance": 14 1524 | }, 1525 | "255": { 1526 | "id": 255, 1527 | "width": 12, 1528 | "height": 23, 1529 | "left": 0, 1530 | "top": -9, 1531 | "advance": 12 1532 | }, 1533 | "256": { 1534 | "id": 256, 1535 | "width": 15, 1536 | "height": 20, 1537 | "left": 0, 1538 | "top": -6, 1539 | "advance": 15 1540 | } 1541 | }, 1542 | "name": "Open Sans Regular", 1543 | "range": "0-256" 1544 | } 1545 | } 1546 | } --------------------------------------------------------------------------------