├── .clang-format ├── .clang-tidy ├── .github └── workflows │ └── builds.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── API.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── DEV.md ├── LICENSE.txt ├── Makefile ├── README.md ├── bench ├── bench.js └── bench2.js ├── bin ├── build-glyphs ├── font-inspect └── namespace ├── binding.gyp ├── common.gypi ├── fonts ├── GuardianTextSansWeb │ ├── GuardianTextSansWeb-Bold.ttf │ └── LICENSE.md ├── firasans-medium │ ├── FiraSans-Medium.ttf │ └── LICENSE ├── open-sans │ ├── Apache License.txt │ └── OpenSans-Regular.ttf └── osaka │ └── Osaka.ttf ├── index.js ├── package-lock.json ├── package.json ├── proto └── glyphs.proto ├── scripts ├── clang-format.sh ├── clang-tidy.sh ├── coverage.sh ├── create_scheme.sh ├── generate_compile_commands.py ├── install_deps.sh ├── library.xcscheme ├── node.xcscheme ├── sanitize.sh └── setup.sh ├── src ├── glyphs.cpp ├── glyphs.hpp └── node_fontnik.cpp └── test ├── bin.test.js ├── composite.test.js ├── expected ├── load.json └── range.json ├── fixtures ├── arialunicode.512.767.pbf ├── fonts-invalid │ ├── 1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf │ └── README.md ├── fonts │ ├── FiraSans-Medium.ttf │ └── OpenSans-Regular.ttf ├── league.512.767.pbf ├── league.opensans.arialunicode.512.767.pbf ├── opensans.512.767.pbf ├── opensans.arialunicode.512.767.pbf └── range.0.256.pbf ├── fontnik.test.js └── format └── glyphs.js /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/builds.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os.host }} 12 | strategy: 13 | matrix: 14 | node: [14, 16, 18] 15 | build_type: ["debug", "release"] 16 | os: 17 | - name: darwin 18 | architecture: x86-64 19 | host: macos-10.15 20 | 21 | - name: linux 22 | architecture: x86-64 23 | host: ubuntu-20.04 24 | 25 | name: ${{ matrix.os.name }}-${{ matrix.os.architecture }}-node${{ matrix.node }}-${{ matrix.build_type }} test 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node }} 31 | 32 | - name: Test 33 | run: | 34 | npm ci 35 | ./scripts/setup.sh --config local.env 36 | source local.env 37 | make ${{ matrix.build_type }} 38 | npm test 39 | 40 | asan-build-test: 41 | runs-on: ubuntu-20.04 42 | name: ASAN toolset test 43 | env: 44 | BUILDTYPE: debug 45 | TOOLSET: asan 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: "14" 51 | 52 | - name: Test 53 | run: | 54 | npm ci 55 | ./scripts/setup.sh --config local.env 56 | source local.env 57 | export CXXFLAGS="${MASON_SANITIZE_CXXFLAGS} -fno-sanitize-recover=all" 58 | export LDFLAGS="${MASON_SANITIZE_LDFLAGS}" 59 | make ${BUILDTYPE} 60 | export LD_PRELOAD=${MASON_LLVM_RT_PRELOAD} 61 | export ASAN_OPTIONS=fast_unwind_on_malloc=0:${ASAN_OPTIONS} 62 | npm test 63 | unset LD_PRELOAD 64 | 65 | g-build-test: 66 | runs-on: ubuntu-20.04 67 | name: G++ build test 68 | env: 69 | BUILDTYPE: debug 70 | CXX: g++-9 71 | CC: gcc-9 72 | CXXFLAGS: -fext-numeric-literals 73 | steps: 74 | - uses: actions/checkout@v3 75 | - uses: actions/setup-node@v3 76 | with: 77 | node-version: "14" 78 | 79 | - name: Test 80 | run: | 81 | npm ci 82 | ./scripts/setup.sh --config local.env 83 | source local.env 84 | make ${BUILDTYPE} 85 | npm test 86 | 87 | build: 88 | needs: [test, asan-build-test, g-build-test] 89 | runs-on: ${{ matrix.os.host }} 90 | strategy: 91 | matrix: 92 | os: 93 | - name: darwin 94 | architecture: x86-64 95 | host: macos-10.15 96 | 97 | - name: linux 98 | architecture: x86-64 99 | host: ubuntu-20.04 100 | 101 | steps: 102 | - uses: actions/checkout@v3 103 | - uses: actions/setup-node@v3 104 | with: 105 | node-version: "16" 106 | 107 | - name: Build 108 | run: | 109 | ./scripts/setup.sh --config local.env 110 | source local.env 111 | make release 112 | 113 | - name: Prebuildify ${{ matrix.os.name }}-${{ matrix.os.architecture }} 114 | run: npm run prebuildify -- --platform=${{ matrix.os.name }} --arch=x64 115 | 116 | # Upload the end-user binary artifact 117 | - uses: actions/upload-artifact@v3 118 | with: 119 | name: prebuilds 120 | path: prebuilds 121 | retention-days: 14 122 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".mason"] 2 | path = .mason 3 | url = https://github.com/mapbox/mason.git 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.2 2 | 3 | - Removes `node-pre-gyp` in favor of `prebuildify` to package binaries. [#184](https://github.com/mapbox/node-fontnik/pull/184) 4 | 5 | # 0.7.1 6 | 7 | - Fixes issue with `point-in-polygon` algorithm being used in `sdf-glyph-foundary` 8 | 9 | # 0.7.0 10 | 11 | - Adds node v16 support 12 | - Updates vulnerable dependencies 13 | 14 | # 0.6.0 15 | 16 | - Adds node v12 and v14 support 17 | - Dropped node v4 and v6 support 18 | - Adds `fontnik.composite` 19 | - Drops `libprotobuf` dependency, uses `protozero` instead 20 | - Requires c++14 compatible compiler 21 | - Binaries are published using clang++ 10.0.0 22 | 23 | # 0.5.2 24 | 25 | - Adds .npmignore to keep downstream node_modules small. 26 | 27 | # 0.5.1 28 | 29 | - Stopped bundling node-pre-gyp 30 | - Added support for node v8 and v10 31 | - Various performance optimizations and safety checks 32 | 33 | # 0.5.0 34 | 35 | - Fixed crash on font with null family name 36 | - Optimized the code to reduce memory allocations 37 | - Now using external https://github.com/mapbox/sdf-glyph-foundry 38 | - Now only building binaries for node v4/v6 39 | - Now publishing debug builds for linux 40 | - Now publishing asan builds for linux 41 | - Upgraded from boost 1.62.0 -> 1.63.0 42 | - Upgraded from freetype 2.6 -> 2.7.1 43 | - Upgraded from protobuf 2.6.1 -> 3.2.0 44 | - Moved coverage reporting to codecov.io 45 | 46 | # 0.4.8 47 | 48 | - Bundles `mkdirp` to avoid an npm@2 bug when using `bundledDependencies` with `devDependencies`. 49 | 50 | # 0.4.7 51 | 52 | - Upgrades to a modern version of Mason. 53 | 54 | # 0.4.6 55 | 56 | - Adds prepublish `npm ls` script to prevent publishing without `bundledDependencies`. 57 | 58 | # 0.4.5 59 | 60 | - Fixes Osaka range segfault. 61 | 62 | # 0.4.4 63 | 64 | - Fix initialization of `queue-async` in `bin/build-glyphs`. 65 | 66 | # 0.4.3 67 | 68 | - Handle `ft_face->style_name` null value in `RangeAsync`. 69 | 70 | # 0.4.2 71 | 72 | - Handle `ft_face->style_name` null value in `LoadAsync`. 73 | 74 | # 0.4.1 75 | 76 | - Publish Node.js v5.x binaries. 77 | - Autopublish binaries on git tags. 78 | 79 | # 0.4.0 80 | 81 | - Fixes bounds for short ranges. 82 | - Fixes Travis binary publishing. 83 | - Adds Node.js v4.x support. 84 | 85 | # 0.2.6 86 | 87 | - Truncate at Unicode point 65535 (0xFFFF) instead of 65533. 88 | 89 | # 0.2.3 90 | 91 | - Calling .codepoints() on an invalid font will throw a JavaScript 92 | error rather than running into an abort trap. 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 successfull. 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ## Background reading 54 | - [Drawing Text with Signed Distance Fields in Mapbox GL](https://www.mapbox.com/blog/text-signed-distance-fields/) 55 | - [State of Text Rendering](http://behdad.org/text/) 56 | - [Pango vs HarfBuzz](http://mces.blogspot.com/2009/11/pango-vs-harfbuzz.html) 57 | - [An Introduction to Writing Systems & Unicode](http://rishida.net/docs/unicode-tutorial/) 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 [[ ! -d ${llvm_toolchain_dir} ]]; then 48 | BINARY="${MASON_URL}/clang++/${MASON_LLVM_RELEASE}.tar.gz" 49 | echo "Downloading ${BINARY}" 50 | mkdir -p ${llvm_toolchain_dir} 51 | curl -sSfL ${BINARY} | tar --gunzip --extract --strip-components=1 --directory=${llvm_toolchain_dir} 52 | fi 53 | 54 | # 55 | # MASON 56 | # 57 | 58 | function setup_mason() { 59 | local install_dir=${1} 60 | local mason_release=${2} 61 | mkdir -p ${install_dir} 62 | curl -sSfL https://github.com/mapbox/mason/archive/${mason_release}.tar.gz | tar --gunzip --extract --strip-components=1 --directory=${install_dir} 63 | } 64 | 65 | setup_mason $(pwd)/.mason ${MASON_RELEASE} 66 | 67 | # 68 | # ENV SETTINGS 69 | # 70 | 71 | echo "export PATH=${llvm_toolchain_dir}/bin:$(pwd)/.mason:$(pwd)/mason_packages/.link/bin:"'${PATH}' > ${config} 72 | echo "export CXX=${CXX:-${llvm_toolchain_dir}/bin/clang++}" >> ${config} 73 | echo "export MASON_RELEASE=${MASON_RELEASE}" >> ${config} 74 | echo "export MASON_LLVM_RELEASE=${MASON_LLVM_RELEASE}" >> ${config} 75 | # https://github.com/google/sanitizers/wiki/AddressSanitizerAsDso 76 | RT_BASE=${llvm_toolchain_dir}/lib/clang/${MASON_LLVM_RELEASE}/lib/$(uname | tr A-Z a-z)/libclang_rt 77 | if [[ $(uname -s) == 'Darwin' ]]; then 78 | RT_PRELOAD=${RT_BASE}.asan_osx_dynamic.dylib 79 | else 80 | RT_PRELOAD=${RT_BASE}.asan-x86_64.so 81 | fi 82 | echo "export MASON_LLVM_RT_PRELOAD=${RT_PRELOAD}" >> ${config} 83 | SUPPRESSION_FILE="/tmp/leak_suppressions.txt" 84 | echo "leak:__strdup" > ${SUPPRESSION_FILE} 85 | echo "leak:v8::internal" >> ${SUPPRESSION_FILE} 86 | echo "leak:node::CreateEnvironment" >> ${SUPPRESSION_FILE} 87 | echo "leak:node::Init" >> ${SUPPRESSION_FILE} 88 | echo "leak:node::Buffer::Copy" >> ${SUPPRESSION_FILE} 89 | echo "export ASAN_SYMBOLIZER_PATH=${llvm_toolchain_dir}/bin/llvm-symbolizer" >> ${config} 90 | echo "export MSAN_SYMBOLIZER_PATH=${llvm_toolchain_dir}/bin/llvm-symbolizer" >> ${config} 91 | echo "export UBSAN_OPTIONS=print_stacktrace=1" >> ${config} 92 | echo "export LSAN_OPTIONS=suppressions=${SUPPRESSION_FILE}" >> ${config} 93 | 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} 94 | echo 'export MASON_SANITIZE="-fsanitize=address,undefined,integer,leak -fno-sanitize=vptr,function"' >> ${config} 95 | echo 'export MASON_SANITIZE_CXXFLAGS="${MASON_SANITIZE} -fno-sanitize=vptr,function -fsanitize-address-use-after-scope -fno-omit-frame-pointer -fno-common"' >> ${config} 96 | echo 'export MASON_SANITIZE_LDFLAGS="${MASON_SANITIZE}"' >> ${config} 97 | 98 | exit 0 99 | } 100 | 101 | function usage() { 102 | >&2 echo "Usage" 103 | >&2 echo "" 104 | >&2 echo "$ ./scripts/setup.sh --config local.env" 105 | >&2 echo "$ source local.env" 106 | >&2 echo "" 107 | exit 1 108 | } 109 | 110 | if [[ ! ${1:-} ]]; then 111 | usage 112 | fi 113 | 114 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 115 | for i in "$@" 116 | do 117 | case $i in 118 | --config) 119 | if [[ ! ${2:-} ]]; then 120 | usage 121 | fi 122 | shift 123 | run $@ 124 | ;; 125 | -h | --help) 126 | usage 127 | shift 128 | ;; 129 | *) 130 | usage 131 | ;; 132 | esac 133 | done 134 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/bin.test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var exec = require('child_process').exec; 4 | var test = require('tape'); 5 | var { queue } = require('d3-queue'); 6 | var mkdirp = require('mkdirp'); 7 | 8 | var bin_output = path.resolve(__dirname + '/bin_output'); 9 | 10 | test('setup', function (t) { 11 | mkdirp(bin_output, function (err) { 12 | t.error(err, 'setup'); 13 | t.end(); 14 | }); 15 | }); 16 | 17 | var registry_invalid = path.normalize(__dirname + '/fixtures/fonts-invalid'); 18 | var registry = path.normalize(__dirname + '/fixtures/fonts'); 19 | 20 | 21 | test('bin/build-glyphs', function (t) { 22 | var script = path.normalize(__dirname + '/../bin/build-glyphs'), 23 | font = path.normalize(__dirname + '/../fonts/open-sans/OpenSans-Regular.ttf'), 24 | dir = path.resolve(__dirname + '/bin_output'); 25 | t.test('outputs expected', function (q) { 26 | exec([script, font, dir].join(' '), function (err, stdout, stderr) { 27 | q.error(err); 28 | if (!process.env.TOOLSET) q.error(stderr); 29 | fs.readdir(bin_output, function (err, files) { 30 | q.equal(files.length, 256, 'outputs 256 files'); 31 | q.equal(files.indexOf('0-255.pbf'), 0, 'expected .pbf'); 32 | q.equal(files.filter(function (f) { 33 | return f.indexOf('.pbf') > -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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /test/fixtures/arialunicode.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/arialunicode.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/fonts-invalid/1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/fonts-invalid/1c2c3fc37b2d4c3cb2ef726c6cdaaabd4b7f3eb9.ttf -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /test/fixtures/fonts/FiraSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/fonts/FiraSans-Medium.ttf -------------------------------------------------------------------------------- /test/fixtures/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /test/fixtures/league.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/league.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/league.opensans.arialunicode.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/league.opensans.arialunicode.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/opensans.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/opensans.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/opensans.arialunicode.512.767.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/opensans.arialunicode.512.767.pbf -------------------------------------------------------------------------------- /test/fixtures/range.0.256.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/node-fontnik/ddffc4f398599539a221af4daf6ef895f98d113b/test/fixtures/range.0.256.pbf -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------