├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── LICENSE ├── README.md ├── build-subset.sh ├── build.sh ├── config-override-subset.h ├── config-override.h ├── examples ├── hb-subset.example.node.js ├── hbjs.example.html ├── hbjs.example.js ├── hbjs.example.node.js └── nohbjs.html ├── harfbuzz.ts ├── hb-subset.symbols ├── hb.symbols ├── hbjs.js ├── index.js ├── package.json ├── test ├── fonts │ └── noto │ │ ├── NotoSans-Regular.otf │ │ ├── NotoSans-Regular.ttf │ │ ├── NotoSansArabic-Variable.ttf │ │ ├── NotoSansDevanagari-Regular.otf │ │ └── OFL.txt └── index.js └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | EM_VERSION: 3.1.56 11 | EM_CACHE_FOLDER: 'emsdk-cache' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | submodules: true 23 | 24 | - name: Setup cache 25 | id: cache-system-libraries 26 | uses: actions/cache@v3 27 | with: 28 | path: ${{env.EM_CACHE_FOLDER}} 29 | key: ${{env.EM_VERSION}}-${{runner.os}} 30 | - name: Setup Emscripten 31 | uses: mymindstorm/setup-emsdk@v14 32 | with: 33 | version: ${{env.EM_VERSION}} 34 | actions-cache-folder: ${{env.EM_CACHE_FOLDER}} 35 | - name: Build hb.wasm 36 | run: ./build.sh 37 | - name: Build hb-subset.wasm 38 | run: ./build-subset.sh 39 | 40 | - name: Setup Node.js 41 | uses: actions/setup-node@v3 42 | - name: NPM install 43 | run: npm install 44 | - name: Run tests 45 | run: npm test 46 | - name: Test hb.wasm 47 | run: node examples/hbjs.example.node.js 48 | - name: Test hb-subset.wasm 49 | run: node examples/hb-subset.example.node.js 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.wasm 2 | hb.js 3 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "harfbuzz"] 2 | path = harfbuzz 3 | url = https://github.com/harfbuzz/harfbuzz.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | harfbuzz/ 2 | .github/ 3 | .gitmodules 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache license for Zephyr libc implementations (zephyr-string.c), 2 | emmalloc.cpp (from emscripten project) and MIT for rest of the project 3 | 4 | Copyright (c) 2019 Ebrahim Byagowi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # harfbuzzjs 2 | Providing [HarfBuzz](https://github.com/harfbuzz/harfbuzz) shaping 3 | library for client/server side JavaScript projects. 4 | 5 | See the demo [here](https://harfbuzz.github.io/harfbuzzjs/). 6 | 7 | ## Building 8 | 1. Install emscripten 9 | 2. `./build.sh` 10 | 11 | ## Download 12 | Download the pack from [releases tab](https://github.com/harfbuzz/harfbuzzjs/releases) 13 | of the project, or just download the [demo page](https://harfbuzz.github.io/harfbuzzjs/) (the 14 | demo source is in [gh-pages](https://github.com/harfbuzz/harfbuzzjs/tree/gh-pages) branch). 15 | 16 | ## Usage and testing 17 | 18 | ### TL;DR 19 | 20 | ```javascript 21 | hb = require("hbjs.js") 22 | WebAssembly.instantiateStreaming(fetch("hb.wasm")).then(function (result) { 23 | fetch('myfont.ttf').then(function (data) { 24 | return data.arrayBuffer(); 25 | }).then(function (fontdata) { 26 | var blob = hb.createBlob(fontdata); // Load the font data into something Harfbuzz can use 27 | var face = hb.createFace(blob, 0); // Select the first font in the file (there's normally only one!) 28 | var font = hb.createFont(face); // Create a Harfbuzz font object from the face 29 | var buffer = hb.createBuffer(); // Make a buffer to hold some text 30 | buffer.addText('abc'); // Fill it with some stuff 31 | buffer.guessSegmentProperties(); // Set script, language and direction 32 | hb.shape(font, buffer); // Shape the text, determining glyph IDs and positions 33 | var output = buffer.json(); 34 | 35 | // Enumerate the glyphs 36 | var xCursor = 0; 37 | var yCursor = 0; 38 | for (glyph of output) { 39 | var glyphId = glyph.g; 40 | var xAdvance = glyph.ax; 41 | var xDisplacement = glyph.dx; 42 | var yDisplacement = glyph.dy; 43 | 44 | var svgPath = font.glyphToPath(glyphId); 45 | // You need to supply this bit 46 | drawAGlyph(svgPath, xCursor + xDisplacement, yDisplacement); 47 | 48 | xCursor += xAdvance; 49 | } 50 | 51 | // Release memory 52 | buffer.destroy(); 53 | font.destroy(); 54 | face.destroy(); 55 | blob.destroy(); 56 | }) 57 | }) 58 | ``` 59 | 60 | More examples: 61 | 62 | ### Browser 63 | 64 | 1. `npx pad.js` 65 | 2. Open `http://127.0.0.1/examples/hbjs.example.html` or `http://127.0.0.1/examples/nohbjs.html` 66 | 67 | ### Node.js 68 | 69 | 1. `(cd examples && node hbjs.example.node.js)` 70 | 71 | We provide a tiny wrapper (`hbjs.js`) around the main functionality of harfbuzz, but it's also easy to use other parts. (See `example/nohbjs.js` as an example. However, you may need a custom build to expose additional functionality.) 72 | 73 | ## [npm](https://www.npmjs.com/package/harfbuzzjs) 74 | Can be added with `npm i harfbuzzjs` or `yarn add harfbuzzjs`, see the examples for 75 | how to use it. 76 | 77 | ## Need more of the library? 78 | 79 | harfbuzzjs uses a stripped-down version of Harfbuzz generated by compiling Harfbuzz with `-DHB_TINY`. This may mean that some functions you need are not available. Look at `src/hb-config.hh` in the Harfbuzz source directory to see what has been removed. For example, `HB_TINY` defines `HB_LEAN` which (amongst other things) defines `HB_NO_OT_GLYPH_NAMES`. If, for example, you really need to get at the glyph names: 80 | 81 | 1. First, undefine the macro in question, by adding e.g. `#undef HB_NO_OT_GLYPH_NAMES` to `config-override.h`. 82 | 2. Next, export any function that you need by adding a line to `hbjs.symbols`; in this case `_hb_ot_get_glyph_name`. 83 | 3. Now the function will be exported through the WASM object, but you need to add Javascript to bridge to it - in this case, handling the memory allocation of the `char *` parameter `name` and marshalling it to a JavaScript string with `heapu8.subarray`. The best way to do this is to look at `hbjs.js` for functions which use similar signatures. 84 | 85 | If you have extended harfbuzzjs in ways that you think others will also benefit from, please raise a pull request. If there are parts of Harfbuzz that you need but the instructions above don't work, describe what you are trying to do in an issue. 86 | 87 | ## Using the library in a bigger emscripten project? 88 | See [harfbuzz port inside emscripten](https://github.com/emscripten-core/emscripten/blob/master/tools/ports/harfbuzz.py) 89 | and [emscripten-ports/HarfBuzz](https://github.com/emscripten-ports/HarfBuzz), basically all you need is to use 90 | `-s USE_HARFBUZZ=1` in your build. 91 | 92 | ## binaryen 93 | 94 | Optionally you can install `binaryen` and use `wasm-opt` like: 95 | 96 | ``` 97 | wasm-opt -Oz hb.wasm -o hb.wasm 98 | ``` 99 | 100 | `binaryen` also provides `wasm-dis` which can be used for, 101 | 102 | ``` 103 | wasm-dis hb.wasm | grep export 104 | wasm-dis hb.wasm | grep import 105 | ``` 106 | 107 | with that you can check if the built wasm file only exports things you need and 108 | doesn't need to import anything, as usual with wasm files built here. 109 | -------------------------------------------------------------------------------- /build-subset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | em++ \ 5 | -std=c++11 \ 6 | -fno-exceptions \ 7 | -fno-rtti \ 8 | -fno-threadsafe-statics \ 9 | -fvisibility-inlines-hidden \ 10 | -Oz \ 11 | -I. \ 12 | -DHB_TINY \ 13 | -DHB_USE_INTERNAL_QSORT \ 14 | -DHB_CONFIG_OVERRIDE_H=\"config-override-subset.h\" \ 15 | -DHB_EXPERIMENTAL_API \ 16 | --no-entry \ 17 | -s EXPORTED_FUNCTIONS=@hb-subset.symbols \ 18 | -s INITIAL_MEMORY=65MB \ 19 | -o hb-subset.wasm \ 20 | harfbuzz/src/harfbuzz-subset.cc 21 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | em++ \ 5 | -std=c++11 \ 6 | -fno-exceptions \ 7 | -fno-rtti \ 8 | -fno-threadsafe-statics \ 9 | -fvisibility-inlines-hidden \ 10 | -flto \ 11 | -Oz \ 12 | -I. \ 13 | -DHB_TINY \ 14 | -DHB_USE_INTERNAL_QSORT \ 15 | -DHB_CONFIG_OVERRIDE_H=\"config-override.h\" \ 16 | -DHB_EXPERIMENTAL_API \ 17 | --no-entry \ 18 | -s MODULARIZE \ 19 | -s EXPORT_NAME=createHarfBuzz \ 20 | -s EXPORTED_FUNCTIONS=@hb.symbols \ 21 | -s EXPORTED_RUNTIME_METHODS='["addFunction", "removeFunction", "wasmMemory", "wasmExports"]' \ 22 | -s INITIAL_MEMORY=256KB \ 23 | -s ALLOW_MEMORY_GROWTH \ 24 | -s ALLOW_TABLE_GROWTH \ 25 | -lexports.js \ 26 | -o hb.js \ 27 | harfbuzz/src/harfbuzz.cc 28 | -------------------------------------------------------------------------------- /config-override-subset.h: -------------------------------------------------------------------------------- 1 | #undef HB_NO_CFF 2 | #undef HB_NO_OT_FONT_CFF 3 | #undef HB_NO_SUBSET_CFF 4 | #undef HB_NO_SUBSET_LAYOUT 5 | #undef HB_NO_VAR 6 | #undef HB_NO_STYLE 7 | #undef HB_NO_VERTICAL -------------------------------------------------------------------------------- /config-override.h: -------------------------------------------------------------------------------- 1 | #undef HB_NO_CFF 2 | #undef HB_NO_OT_FONT_CFF 3 | #undef HB_NO_DRAW 4 | #undef HB_NO_BUFFER_MESSAGE 5 | #undef HB_NO_BUFFER_SERIALIZE 6 | #undef HB_NO_VAR 7 | #undef HB_NO_OT_FONT_GLYPH_NAMES 8 | #undef HB_NO_FACE_COLLECT_UNICODES 9 | #undef HB_NO_AVAR2 10 | #undef HB_NO_CUBIC_GLYF 11 | #undef HB_NO_VAR_COMPOSITES 12 | -------------------------------------------------------------------------------- /examples/hb-subset.example.node.js: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/harfbuzz/harfbuzzjs/issues/9#issuecomment-507874962 2 | // Which was based on https://github.com/harfbuzz/harfbuzzjs/issues/9#issuecomment-507622485 3 | const { readFile, writeFile } = require('fs').promises; 4 | const { join, extname, basename } = require('path'); 5 | const { performance } = require('node:perf_hooks'); 6 | 7 | const SUBSET_TEXT = 'abc'; 8 | 9 | (async () => { 10 | const { instance: { exports } } = await WebAssembly.instantiate(await readFile(join(__dirname, '../hb-subset.wasm'))); 11 | const fileName = 'NotoSans-Regular.ttf'; 12 | const fontBlob = await readFile(join(__dirname, '../test/fonts/noto', fileName)); 13 | 14 | const t = performance.now(); 15 | const heapu8 = new Uint8Array(exports.memory.buffer); 16 | const fontBuffer = exports.malloc(fontBlob.byteLength); 17 | heapu8.set(new Uint8Array(fontBlob), fontBuffer); 18 | 19 | /* Creating a face */ 20 | const blob = exports.hb_blob_create(fontBuffer, fontBlob.byteLength, 2/*HB_MEMORY_MODE_WRITABLE*/, 0, 0); 21 | const face = exports.hb_face_create(blob, 0); 22 | exports.hb_blob_destroy(blob); 23 | 24 | /* Add your glyph indices here and subset */ 25 | const input = exports.hb_subset_input_create_or_fail(); 26 | const unicode_set = exports.hb_subset_input_unicode_set(input); 27 | for (const text of SUBSET_TEXT) { 28 | exports.hb_set_add(unicode_set, text.codePointAt(0)); 29 | } 30 | 31 | // exports.hb_subset_input_set_drop_hints(input, true); 32 | const subset = exports.hb_subset_or_fail(face, input); 33 | 34 | /* Clean up */ 35 | exports.hb_subset_input_destroy(input); 36 | 37 | /* Get result blob */ 38 | const resultBlob = exports.hb_face_reference_blob(subset); 39 | 40 | const offset = exports.hb_blob_get_data(resultBlob, 0); 41 | const subsetByteLength = exports.hb_blob_get_length(resultBlob); 42 | if (subsetByteLength === 0) { 43 | exports.hb_blob_destroy(resultBlob); 44 | exports.hb_face_destroy(subset); 45 | exports.hb_face_destroy(face); 46 | exports.free(fontBuffer); 47 | throw new Error( 48 | 'Failed to create subset font, maybe the input file is corrupted?' 49 | ); 50 | } 51 | 52 | // Output font data(Uint8Array) 53 | const subsetFontBlob = heapu8.subarray(offset, offset + exports.hb_blob_get_length(resultBlob)); 54 | console.info('✨ Subset done in', performance.now() - t, 'ms'); 55 | 56 | const extName = extname(fileName).toLowerCase(); 57 | const fontName = basename(fileName, extName); 58 | await writeFile(join(__dirname, '/', `${fontName}.subset${extName}`), subsetFontBlob); 59 | console.info(`Wrote subset to: ${__dirname}/${fontName}.subset${extName}`); 60 | 61 | /* Clean up */ 62 | exports.hb_blob_destroy(resultBlob); 63 | exports.hb_face_destroy(subset); 64 | exports.hb_face_destroy(face); 65 | exports.free(fontBuffer); 66 | })(); 67 | -------------------------------------------------------------------------------- /examples/hbjs.example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | -------------------------------------------------------------------------------- /examples/hbjs.example.js: -------------------------------------------------------------------------------- 1 | function example(hb, fontBlob, text) { 2 | var blob = hb.createBlob(fontBlob); 3 | var face = hb.createFace(blob, 0); 4 | // console.log(face.getAxisInfos()); 5 | var font = hb.createFont(face); 6 | // font.setVariations({ wdth: 200, wght: 700 }); 7 | font.setScale(1000, 1000); // Optional, if not given will be in font upem 8 | 9 | var buffer = hb.createBuffer(); 10 | buffer.addText(text || 'abc'); 11 | buffer.guessSegmentProperties(); 12 | // buffer.setDirection('ltr'); // optional as can be set by guessSegmentProperties also 13 | hb.shape(font, buffer); // features are not supported yet 14 | var result = buffer.json(font); 15 | 16 | // returns glyphs paths, totally optional 17 | var glyphs = {}; 18 | result.forEach(function (x) { 19 | if (glyphs[x.g]) return; 20 | glyphs[x.g] = { 21 | name: font.glyphName(x.g), 22 | path: font.glyphToPath(x.g), 23 | json: font.glyphToJson(x.g) 24 | }; 25 | }); 26 | 27 | var unicodes = face.collectUnicodes() 28 | 29 | buffer.destroy(); 30 | font.destroy(); 31 | face.destroy(); 32 | blob.destroy(); 33 | return { shape: result, glyphs: glyphs, unicodes: unicodes }; 34 | } 35 | 36 | // Should be replaced with something more reliable 37 | try { module.exports = example; } catch(e) {} 38 | -------------------------------------------------------------------------------- /examples/hbjs.example.node.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var example = require('./hbjs.example.js'); 4 | 5 | require('../').then(function (hbjs) { 6 | console.log(example(hbjs, new Uint8Array(fs.readFileSync(path.resolve(__dirname, '../test/fonts/noto/NotoSans-Regular.ttf'))))); 7 | console.log(example(hbjs, new Uint8Array(fs.readFileSync(path.resolve(__dirname, '../test/fonts/noto/NotoSansArabic-Variable.ttf'))), "أبجد")); 8 | }, console.log); 9 | -------------------------------------------------------------------------------- /examples/nohbjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 65 | -------------------------------------------------------------------------------- /harfbuzz.ts: -------------------------------------------------------------------------------- 1 | type Pointer = number; 2 | 3 | const HB_MEMORY_MODE_WRITABLE: number = 2; 4 | const HB_SET_VALUE_INVALID: Pointer = -1; 5 | 6 | class HarfBuzzExports { 7 | readonly heapu8: Uint8Array; 8 | readonly heapu32: Uint32Array; 9 | readonly heapi32: Int32Array; 10 | readonly utf8Encoder: TextEncoder; 11 | 12 | //exported HarfBuzz methods 13 | readonly malloc: (length: number) => Pointer 14 | readonly free: (ptr: Pointer) => void 15 | readonly free_ptr: () => Pointer 16 | readonly hb_blob_create: (data: Pointer, length: number, memoryMode: number, useData: Pointer, destroyFunction: Pointer) => Pointer 17 | readonly hb_blob_destroy: (ptr: Pointer) => void 18 | readonly hb_face_create: (blobPtr: Pointer, index: number) => Pointer 19 | readonly hb_face_get_upem: (facePtr: Pointer) => number 20 | readonly hb_face_destroy: (ptr: Pointer) => void 21 | readonly hb_font_create: (facePtr: Pointer) => Pointer 22 | readonly hb_font_set_scale: (fontPtr: Pointer, xScale: number, yScale: number) => void 23 | readonly hb_font_destroy: (ptr: Pointer) => void 24 | readonly hb_face_collect_unicodes: (facePtr: Pointer, setPtr: Pointer) => void 25 | readonly hb_set_create: () => Pointer 26 | readonly hb_set_destroy: (setPtr: Pointer) => void 27 | readonly hb_set_get_population: (setPtr: Pointer) => number 28 | readonly hb_set_next_many: ( 29 | setPtr: Pointer, 30 | greaterThanUnicodePtr: Pointer, 31 | outputU32ArrayPtr: Pointer, 32 | size: number, 33 | ) => number 34 | readonly hb_buffer_create: () => Pointer 35 | readonly hb_buffer_add_utf8: (bufferPtr: Pointer, stringPtr: Pointer, stringLength: number, itemOffset: number, itemLength: number) => void 36 | readonly hb_buffer_guess_segment_properties: (bufferPtr: Pointer) => void 37 | readonly hb_buffer_set_direction: (bufferPtr: Pointer, direction: number) => void 38 | readonly hb_shape: (fontPtr: Pointer, bufferPtr: Pointer, features: any, numFeatures: number) => void 39 | readonly hb_buffer_get_length: (bufferPtr: Pointer) => number 40 | readonly hb_buffer_get_glyph_infos: (bufferPtr: Pointer, length: number) => any 41 | readonly hb_buffer_get_glyph_positions: (bufferPtr: Pointer, length: number) => any 42 | readonly hb_buffer_destroy: (bufferPtr: Pointer) => void 43 | 44 | constructor(exports: any) { 45 | this.heapu8 = new Uint8Array(exports.memory.buffer); 46 | this.heapu32 = new Uint32Array(exports.memory.buffer); 47 | this.heapi32 = new Int32Array(exports.memory.buffer); 48 | this.utf8Encoder = new TextEncoder(); 49 | 50 | this.malloc = exports.malloc; 51 | this.free = exports.free; 52 | this.free_ptr = exports.free_ptr; 53 | this.hb_blob_destroy = exports.hb_blob_destroy; 54 | this.hb_blob_create = exports.hb_blob_create; 55 | this.hb_face_create = exports.hb_face_create; 56 | this.hb_face_get_upem = exports.hb_face_get_upem; 57 | this.hb_face_destroy = exports.hb_face_destroy; 58 | this.hb_face_collect_unicodes = exports.hb_face_collect_unicodes; 59 | this.hb_set_create = exports.hb_set_create; 60 | this.hb_set_destroy = exports.hb_set_destroy; 61 | this.hb_set_get_population = exports.hb_set_get_population; 62 | this.hb_set_next_many = exports.hb_set_next_many; 63 | this.hb_font_create = exports.hb_font_create; 64 | this.hb_font_set_scale = exports.hb_font_set_scale; 65 | this.hb_font_destroy = exports.hb_font_destroy; 66 | this.hb_buffer_create = exports.hb_buffer_create; 67 | this.hb_buffer_add_utf8 = exports.hb_buffer_add_utf8; 68 | this.hb_buffer_guess_segment_properties = exports.hb_buffer_guess_segment_properties; 69 | this.hb_buffer_set_direction = exports.hb_buffer_set_direction; 70 | this.hb_shape = exports.hb_shape; 71 | this.hb_buffer_get_length = exports.hb_buffer_get_length; 72 | this.hb_buffer_get_glyph_infos = exports.hb_buffer_get_glyph_infos; 73 | this.hb_buffer_get_glyph_positions = exports.hb_buffer_get_glyph_positions; 74 | this.hb_buffer_destroy = exports.hb_buffer_destroy; 75 | } 76 | 77 | } 78 | 79 | let hb: HarfBuzzExports; 80 | 81 | class CString { 82 | readonly ptr: Pointer; 83 | readonly length: number; 84 | 85 | constructor(text: string) { 86 | var bytes = hb.utf8Encoder.encode(text); 87 | this.ptr = hb.malloc(bytes.byteLength); 88 | hb.heapu8.set(bytes, this.ptr); 89 | this.length = bytes.byteLength; 90 | } 91 | 92 | destroy() { 93 | hb.free(this.ptr); 94 | } 95 | } 96 | 97 | export class HarfBuzzBlob { 98 | readonly ptr: Pointer; 99 | 100 | constructor(data: Uint8Array) { 101 | let blobPtr = hb.malloc(data.length); 102 | hb.heapu8.set(data, blobPtr); 103 | this.ptr = hb.hb_blob_create(blobPtr, data.byteLength, HB_MEMORY_MODE_WRITABLE, blobPtr, hb.free_ptr()); 104 | } 105 | 106 | destroy() { 107 | hb.hb_blob_destroy(this.ptr); 108 | } 109 | } 110 | 111 | function typedArrayFromSet(setPtr: Pointer, arrayType: T) { 112 | const heap = hb[`heap${arrayType}`]; 113 | const bytesPerElment = heap.BYTES_PER_ELEMENT; 114 | const setCount = hb.hb_set_get_population(setPtr); 115 | const arrayPtr = hb.malloc( 116 | setCount * bytesPerElment, 117 | ); 118 | const arrayOffset = arrayPtr / bytesPerElment; 119 | const array = heap.subarray( 120 | arrayOffset, 121 | arrayOffset + setCount, 122 | ) as typeof hb[`heap${T}`]; 123 | heap.set(array, arrayOffset); 124 | hb.hb_set_next_many( 125 | setPtr, 126 | HB_SET_VALUE_INVALID, 127 | arrayPtr, 128 | setCount, 129 | ); 130 | return array; 131 | } 132 | 133 | export class HarfBuzzFace { 134 | readonly ptr: Pointer; 135 | 136 | constructor(blob: HarfBuzzBlob, index: number) { 137 | this.ptr = hb.hb_face_create(blob.ptr, index); 138 | } 139 | 140 | getUnitsPerEM() { 141 | return hb.hb_face_get_upem(this.ptr); 142 | } 143 | 144 | collectUnicodes() { 145 | const unicodeSetPtr = hb.hb_set_create(); 146 | hb.hb_face_collect_unicodes(this.ptr, unicodeSetPtr); 147 | const result = typedArrayFromSet(unicodeSetPtr, 'u32'); 148 | hb.hb_set_destroy(unicodeSetPtr); 149 | return result; 150 | } 151 | 152 | destroy() { 153 | hb.hb_face_destroy(this.ptr); 154 | } 155 | } 156 | 157 | export class HarfBuzzFont { 158 | readonly ptr: Pointer 159 | readonly unitsPerEM: number 160 | 161 | constructor(face: HarfBuzzFace) { 162 | this.ptr = hb.hb_font_create(face.ptr); 163 | this.unitsPerEM = face.getUnitsPerEM(); 164 | } 165 | 166 | setScale(xScale: number, yScale: number) { 167 | hb.hb_font_set_scale(this.ptr, xScale, yScale); 168 | } 169 | 170 | destroy() { 171 | hb.hb_font_destroy(this.ptr); 172 | } 173 | } 174 | 175 | export type HarfBuzzDirection = "ltr" | "rtl" | "ttb" | "btt" 176 | 177 | class GlyphInformation { 178 | readonly GlyphId: number 179 | readonly Cluster: number 180 | readonly XAdvance: number 181 | readonly YAdvance: number 182 | readonly XOffset: number 183 | readonly YOffset: number 184 | 185 | constructor(glyphId: number, cluster: number, xAdvance: number, yAdvance: number, xOffset: number, yOffset: number) { 186 | this.GlyphId = glyphId; 187 | this.Cluster = cluster; 188 | this.XAdvance = xAdvance; 189 | this.YAdvance = yAdvance; 190 | this.XOffset = xOffset; 191 | this.YOffset = yOffset; 192 | } 193 | } 194 | 195 | export class HarfBuzzBuffer { 196 | readonly ptr: Pointer 197 | 198 | constructor() { 199 | this.ptr = hb.hb_buffer_create(); 200 | } 201 | 202 | addText(text: string) { 203 | let str = new CString(text); 204 | hb.hb_buffer_add_utf8(this.ptr, str.ptr, str.length, 0, str.length); 205 | str.destroy(); 206 | } 207 | 208 | guessSegmentProperties() { 209 | hb.hb_buffer_guess_segment_properties(this.ptr); 210 | } 211 | 212 | setDirection(direction: HarfBuzzDirection) { 213 | let d = { "ltr": 4, "rtl": 5, "ttb": 6, "btt": 7 }[direction]; 214 | hb.hb_buffer_set_direction(this.ptr, d); 215 | } 216 | 217 | json() { 218 | var length = hb.hb_buffer_get_length(this.ptr); 219 | var result = new Array(); 220 | var infosPtr32 = hb.hb_buffer_get_glyph_infos(this.ptr, 0) / 4; 221 | var positionsPtr32 = hb.hb_buffer_get_glyph_positions(this.ptr, 0) / 4; 222 | var infos = hb.heapu32.subarray(infosPtr32, infosPtr32 + 5 * length); 223 | var positions = hb.heapi32.subarray(positionsPtr32, positionsPtr32 + 5 * length); 224 | for (var i = 0; i < length; ++i) { 225 | result.push(new GlyphInformation( 226 | infos[i * 5 + 0], 227 | infos[i * 5 + 2], 228 | positions[i * 5 + 0], 229 | positions[i * 5 + 1], 230 | positions[i * 5 + 2], 231 | positions[i * 5 + 3])); 232 | } 233 | return result; 234 | } 235 | 236 | destroy() { 237 | hb.hb_buffer_destroy(this.ptr) 238 | } 239 | } 240 | 241 | export function shape(text: string, font: HarfBuzzFont, features: any): Array { 242 | let buffer = new HarfBuzzBuffer(); 243 | buffer.addText(text); 244 | buffer.guessSegmentProperties(); 245 | buffer.shape(font, features); 246 | let result = buffer.json(); 247 | buffer.destroy(); 248 | return result; 249 | } 250 | 251 | export function getWidth(text: string, font: HarfBuzzFont, fontSizeInPixel: number, features: any): number { 252 | let scale = fontSizeInPixel / font.unitsPerEM; 253 | let shapeResult = shape(text, font, features); 254 | let totalWidth = shapeResult.map((glyphInformation) => { 255 | return glyphInformation.XAdvance; 256 | }).reduce((previous, current, i, arr) => { 257 | return previous + current; 258 | }, 0.0); 259 | 260 | return totalWidth * scale; 261 | } 262 | 263 | export const harfbuzzFonts = new Map(); 264 | 265 | export function loadHarfbuzz(webAssemblyUrl: string): Promise { 266 | return fetch(webAssemblyUrl).then(response => { 267 | return response.arrayBuffer(); 268 | }).then(wasm => { 269 | return WebAssembly.instantiate(wasm); 270 | }).then(result => { 271 | //@ts-ignore 272 | hb = new HarfBuzzExports(result.instance.exports); 273 | }); 274 | } 275 | 276 | export function loadAndCacheFont(fontName: string, fontUrl: string): Promise { 277 | return fetch(fontUrl).then((response) => { 278 | return response.arrayBuffer().then((blob) => { 279 | let fontBlob = new Uint8Array(blob); 280 | let harfbuzzBlob = new HarfBuzzBlob(fontBlob); 281 | let harfbuzzFace = new HarfBuzzFace(harfbuzzBlob, 0); 282 | let harfbuzzFont = new HarfBuzzFont(harfbuzzFace); 283 | 284 | harfbuzzFonts.set(fontName, harfbuzzFont); 285 | harfbuzzFace.destroy(); 286 | harfbuzzBlob.destroy(); 287 | }); 288 | }); 289 | } -------------------------------------------------------------------------------- /hb-subset.symbols: -------------------------------------------------------------------------------- 1 | _hb_blob_create 2 | _hb_blob_destroy 3 | _hb_blob_get_data 4 | _hb_blob_get_length 5 | _hb_face_create 6 | _hb_face_destroy 7 | _hb_face_get_empty 8 | _hb_face_reference_blob 9 | _hb_set_add 10 | _hb_set_clear 11 | _hb_set_create 12 | _hb_set_del 13 | _hb_set_destroy 14 | _hb_set_invert 15 | _hb_set_union 16 | _hb_subset_input_create_or_fail 17 | _hb_subset_input_destroy 18 | _hb_subset_input_get_flags 19 | _hb_subset_input_get_user_data 20 | _hb_subset_input_glyph_set 21 | _hb_subset_input_pin_all_axes_to_default 22 | _hb_subset_input_pin_axis_location 23 | _hb_subset_input_pin_axis_to_default 24 | _hb_subset_input_reference 25 | _hb_subset_input_set 26 | _hb_subset_input_set_axis_range 27 | _hb_subset_input_set_flags 28 | _hb_subset_input_set_user_data 29 | _hb_subset_input_unicode_set 30 | _hb_subset_input_keep_everything 31 | _hb_subset_or_fail 32 | _hb_subset_preprocess 33 | _malloc 34 | _free 35 | -------------------------------------------------------------------------------- /hb.symbols: -------------------------------------------------------------------------------- 1 | _hb_blob_create 2 | _hb_blob_destroy 3 | _hb_blob_get_data 4 | _hb_blob_get_length 5 | _hb_buffer_add_utf16 6 | _hb_buffer_add_utf8 7 | _hb_buffer_create 8 | _hb_buffer_destroy 9 | _hb_buffer_get_glyph_infos 10 | _hb_buffer_get_glyph_positions 11 | _hb_buffer_get_length 12 | _hb_buffer_get_content_type 13 | _hb_buffer_guess_segment_properties 14 | _hb_buffer_set_cluster_level 15 | _hb_buffer_set_direction 16 | _hb_buffer_set_flags 17 | _hb_buffer_set_language 18 | _hb_buffer_set_script 19 | _hb_buffer_set_message_func 20 | _hb_buffer_serialize_glyphs 21 | _hb_face_create 22 | _hb_face_collect_unicodes 23 | _hb_face_destroy 24 | _hb_face_get_upem 25 | _hb_face_reference_table 26 | _hb_font_create 27 | _hb_font_destroy 28 | _hb_font_glyph_to_string 29 | _hb_font_set_scale 30 | _hb_font_set_variations 31 | _hb_font_draw_glyph 32 | _hb_draw_funcs_create 33 | _hb_draw_funcs_destroy 34 | _hb_draw_funcs_set_move_to_func 35 | _hb_draw_funcs_set_line_to_func 36 | _hb_draw_funcs_set_quadratic_to_func 37 | _hb_draw_funcs_set_cubic_to_func 38 | _hb_draw_funcs_set_close_path_func 39 | _hb_glyph_info_get_glyph_flags 40 | _hb_language_from_string 41 | _hb_ot_var_get_axis_infos 42 | _hb_script_from_string 43 | _hb_feature_from_string 44 | _hb_set_create 45 | _hb_set_destroy 46 | _hb_set_get_population 47 | _hb_set_next_many 48 | _hb_shape 49 | _hb_version 50 | _hb_version_string 51 | _malloc 52 | _free 53 | -------------------------------------------------------------------------------- /hbjs.js: -------------------------------------------------------------------------------- 1 | function hbjs(Module) { 2 | 'use strict'; 3 | 4 | var exports = Module.wasmExports; 5 | var utf8Decoder = new TextDecoder("utf8"); 6 | let addFunction = Module.addFunction; 7 | let removeFunction = Module.removeFunction; 8 | 9 | var freeFuncPtr = addFunction(function (ptr) { exports.free(ptr); }, 'vi'); 10 | 11 | var HB_MEMORY_MODE_WRITABLE = 2; 12 | var HB_SET_VALUE_INVALID = -1; 13 | var HB_BUFFER_CONTENT_TYPE_GLYPHS = 2; 14 | var DONT_STOP = 0; 15 | var GSUB_PHASE = 1; 16 | var GPOS_PHASE = 2; 17 | 18 | function hb_tag(s) { 19 | return ( 20 | (s.charCodeAt(0) & 0xFF) << 24 | 21 | (s.charCodeAt(1) & 0xFF) << 16 | 22 | (s.charCodeAt(2) & 0xFF) << 8 | 23 | (s.charCodeAt(3) & 0xFF) << 0 24 | ); 25 | } 26 | 27 | var HB_BUFFER_SERIALIZE_FORMAT_JSON = hb_tag('JSON'); 28 | var HB_BUFFER_SERIALIZE_FLAG_NO_GLYPH_NAMES = 4; 29 | 30 | function _hb_untag(tag) { 31 | return [ 32 | String.fromCharCode((tag >> 24) & 0xFF), 33 | String.fromCharCode((tag >> 16) & 0xFF), 34 | String.fromCharCode((tag >> 8) & 0xFF), 35 | String.fromCharCode((tag >> 0) & 0xFF) 36 | ].join(''); 37 | } 38 | 39 | function _buffer_flag(s) { 40 | if (s == "BOT") { return 0x1; } 41 | if (s == "EOT") { return 0x2; } 42 | if (s == "PRESERVE_DEFAULT_IGNORABLES") { return 0x4; } 43 | if (s == "REMOVE_DEFAULT_IGNORABLES") { return 0x8; } 44 | if (s == "DO_NOT_INSERT_DOTTED_CIRCLE") { return 0x10; } 45 | if (s == "PRODUCE_UNSAFE_TO_CONCAT") { return 0x40; } 46 | return 0x0; 47 | } 48 | 49 | /** 50 | * Create an object representing a Harfbuzz blob. 51 | * @param {string} blob A blob of binary data (usually the contents of a font file). 52 | **/ 53 | function createBlob(blob) { 54 | var blobPtr = exports.malloc(blob.byteLength); 55 | Module.HEAPU8.set(new Uint8Array(blob), blobPtr); 56 | var ptr = exports.hb_blob_create(blobPtr, blob.byteLength, HB_MEMORY_MODE_WRITABLE, blobPtr, freeFuncPtr); 57 | return { 58 | ptr: ptr, 59 | /** 60 | * Free the object. 61 | */ 62 | destroy: function () { exports.hb_blob_destroy(ptr); } 63 | }; 64 | } 65 | 66 | /** 67 | * Return the typed array of HarfBuzz set contents. 68 | * @param {number} setPtr Pointer of set 69 | * @returns {Uint32Array} Typed array instance 70 | */ 71 | function typedArrayFromSet(setPtr) { 72 | const setCount = exports.hb_set_get_population(setPtr); 73 | const arrayPtr = exports.malloc(setCount << 2); 74 | const arrayOffset = arrayPtr >> 2; 75 | const array = Module.HEAPU32.subarray(arrayOffset, arrayOffset + setCount); 76 | Module.HEAPU32.set(array, arrayOffset); 77 | exports.hb_set_next_many(setPtr, HB_SET_VALUE_INVALID, arrayPtr, setCount); 78 | return array; 79 | } 80 | 81 | /** 82 | * Create an object representing a Harfbuzz face. 83 | * @param {object} blob An object returned from `createBlob`. 84 | * @param {number} index The index of the font in the blob. (0 for most files, 85 | * or a 0-indexed font number if the `blob` came form a TTC/OTC file.) 86 | **/ 87 | function createFace(blob, index) { 88 | var ptr = exports.hb_face_create(blob.ptr, index); 89 | const upem = exports.hb_face_get_upem(ptr); 90 | return { 91 | ptr: ptr, 92 | upem, 93 | /** 94 | * Return the binary contents of an OpenType table. 95 | * @param {string} table Table name 96 | */ 97 | reference_table: function(table) { 98 | var blob = exports.hb_face_reference_table(ptr, hb_tag(table)); 99 | var length = exports.hb_blob_get_length(blob); 100 | if (!length) { return; } 101 | var blobptr = exports.hb_blob_get_data(blob, null); 102 | var table_string = Module.HEAPU8.subarray(blobptr, blobptr+length); 103 | return table_string; 104 | }, 105 | /** 106 | * Return variation axis infos 107 | */ 108 | getAxisInfos: function() { 109 | var axis = exports.malloc(64 * 32); 110 | var c = exports.malloc(4); 111 | Module.HEAPU32[c / 4] = 64; 112 | exports.hb_ot_var_get_axis_infos(ptr, 0, c, axis); 113 | var result = {}; 114 | Array.from({ length: Module.HEAPU32[c / 4] }).forEach(function (_, i) { 115 | result[_hb_untag(Module.HEAPU32[axis / 4 + i * 8 + 1])] = { 116 | min: Module.HEAPF32[axis / 4 + i * 8 + 4], 117 | default: Module.HEAPF32[axis / 4 + i * 8 + 5], 118 | max: Module.HEAPF32[axis / 4 + i * 8 + 6] 119 | }; 120 | }); 121 | exports.free(c); 122 | exports.free(axis); 123 | return result; 124 | }, 125 | /** 126 | * Return unicodes the face supports 127 | */ 128 | collectUnicodes: function() { 129 | var unicodeSetPtr = exports.hb_set_create(); 130 | exports.hb_face_collect_unicodes(ptr, unicodeSetPtr); 131 | var result = typedArrayFromSet(unicodeSetPtr); 132 | exports.hb_set_destroy(unicodeSetPtr); 133 | return result; 134 | }, 135 | /** 136 | * Free the object. 137 | */ 138 | destroy: function () { 139 | exports.hb_face_destroy(ptr); 140 | }, 141 | }; 142 | } 143 | 144 | var pathBuffer = ""; 145 | 146 | var nameBufferSize = 256; // should be enough for most glyphs 147 | var nameBuffer = exports.malloc(nameBufferSize); // permanently allocated 148 | 149 | /** 150 | * Create an object representing a Harfbuzz font. 151 | * @param {object} blob An object returned from `createFace`. 152 | **/ 153 | function createFont(face) { 154 | var ptr = exports.hb_font_create(face.ptr); 155 | var drawFuncsPtr = null; 156 | var moveToPtr = null; 157 | var lineToPtr = null; 158 | var cubicToPtr = null; 159 | var quadToPtr = null; 160 | var closePathPtr = null; 161 | 162 | /** 163 | * Return a glyph as an SVG path string. 164 | * @param {number} glyphId ID of the requested glyph in the font. 165 | **/ 166 | function glyphToPath(glyphId) { 167 | if (!drawFuncsPtr) { 168 | var moveTo = function (dfuncs, draw_data, draw_state, to_x, to_y, user_data) { 169 | pathBuffer += `M${to_x},${to_y}`; 170 | } 171 | var lineTo = function (dfuncs, draw_data, draw_state, to_x, to_y, user_data) { 172 | pathBuffer += `L${to_x},${to_y}`; 173 | } 174 | var cubicTo = function (dfuncs, draw_data, draw_state, c1_x, c1_y, c2_x, c2_y, to_x, to_y, user_data) { 175 | pathBuffer += `C${c1_x},${c1_y} ${c2_x},${c2_y} ${to_x},${to_y}`; 176 | } 177 | var quadTo = function (dfuncs, draw_data, draw_state, c_x, c_y, to_x, to_y, user_data) { 178 | pathBuffer += `Q${c_x},${c_y} ${to_x},${to_y}`; 179 | } 180 | var closePath = function (dfuncs, draw_data, draw_state, user_data) { 181 | pathBuffer += 'Z'; 182 | } 183 | 184 | moveToPtr = addFunction(moveTo, 'viiiffi'); 185 | lineToPtr = addFunction(lineTo, 'viiiffi'); 186 | cubicToPtr = addFunction(cubicTo, 'viiiffffffi'); 187 | quadToPtr = addFunction(quadTo, 'viiiffffi'); 188 | closePathPtr = addFunction(closePath, 'viiii'); 189 | drawFuncsPtr = exports.hb_draw_funcs_create(); 190 | exports.hb_draw_funcs_set_move_to_func(drawFuncsPtr, moveToPtr, 0, 0); 191 | exports.hb_draw_funcs_set_line_to_func(drawFuncsPtr, lineToPtr, 0, 0); 192 | exports.hb_draw_funcs_set_cubic_to_func(drawFuncsPtr, cubicToPtr, 0, 0); 193 | exports.hb_draw_funcs_set_quadratic_to_func(drawFuncsPtr, quadToPtr, 0, 0); 194 | exports.hb_draw_funcs_set_close_path_func(drawFuncsPtr, closePathPtr, 0, 0); 195 | } 196 | 197 | pathBuffer = ""; 198 | exports.hb_font_draw_glyph(ptr, glyphId, drawFuncsPtr, 0); 199 | return pathBuffer; 200 | } 201 | 202 | /** 203 | * Return glyph name. 204 | * @param {number} glyphId ID of the requested glyph in the font. 205 | **/ 206 | function glyphName(glyphId) { 207 | exports.hb_font_glyph_to_string( 208 | ptr, 209 | glyphId, 210 | nameBuffer, 211 | nameBufferSize 212 | ); 213 | var array = Module.HEAPU8.subarray(nameBuffer, nameBuffer + nameBufferSize); 214 | return utf8Decoder.decode(array.slice(0, array.indexOf(0))); 215 | } 216 | 217 | return { 218 | ptr: ptr, 219 | glyphName: glyphName, 220 | glyphToPath: glyphToPath, 221 | /** 222 | * Return a glyph as a JSON path string 223 | * based on format described on https://svgwg.org/specs/paths/#InterfaceSVGPathSegment 224 | * @param {number} glyphId ID of the requested glyph in the font. 225 | **/ 226 | glyphToJson: function (glyphId) { 227 | var path = glyphToPath(glyphId); 228 | return path.replace(/([MLQCZ])/g, '|$1 ').split('|').filter(function (x) { return x.length; }).map(function (x) { 229 | var row = x.split(/[ ,]/g); 230 | return { type: row[0], values: row.slice(1).filter(function (x) { return x.length; }).map(function (x) { return +x; }) }; 231 | }); 232 | }, 233 | /** 234 | * Set the font's scale factor, affecting the position values returned from 235 | * shaping. 236 | * @param {number} xScale Units to scale in the X dimension. 237 | * @param {number} yScale Units to scale in the Y dimension. 238 | **/ 239 | setScale: function (xScale, yScale) { 240 | exports.hb_font_set_scale(ptr, xScale, yScale); 241 | }, 242 | /** 243 | * Set the font's variations. 244 | * @param {object} variations Dictionary of variations to set 245 | **/ 246 | setVariations: function (variations) { 247 | var entries = Object.entries(variations); 248 | var vars = exports.malloc(8 * entries.length); 249 | entries.forEach(function (entry, i) { 250 | Module.HEAPU32[vars / 4 + i * 2 + 0] = hb_tag(entry[0]); 251 | Module.HEAPF32[vars / 4 + i * 2 + 1] = entry[1]; 252 | }); 253 | exports.hb_font_set_variations(ptr, vars, entries.length); 254 | exports.free(vars); 255 | }, 256 | /** 257 | * Free the object. 258 | */ 259 | destroy: function () { 260 | exports.hb_font_destroy(ptr); 261 | if (drawFuncsPtr) { 262 | exports.hb_draw_funcs_destroy(drawFuncsPtr); 263 | drawFuncsPtr = null; 264 | removeFunction(moveToPtr); 265 | removeFunction(lineToPtr); 266 | removeFunction(cubicToPtr); 267 | removeFunction(quadToPtr); 268 | removeFunction(closePathPtr); 269 | } 270 | } 271 | }; 272 | } 273 | 274 | /** 275 | * Use when you know the input range should be ASCII. 276 | * Faster than encoding to UTF-8 277 | **/ 278 | function createAsciiString(text) { 279 | var ptr = exports.malloc(text.length + 1); 280 | for (let i = 0; i < text.length; ++i) { 281 | const char = text.charCodeAt(i); 282 | if (char > 127) throw new Error('Expected ASCII text'); 283 | Module.HEAPU8[ptr + i] = char; 284 | } 285 | Module.HEAPU8[ptr + text.length] = 0; 286 | return { 287 | ptr: ptr, 288 | length: text.length, 289 | free: function () { exports.free(ptr); } 290 | }; 291 | } 292 | 293 | function createJsString(text) { 294 | const ptr = exports.malloc(text.length * 2); 295 | const words = new Uint16Array(Module.wasmMemory.buffer, ptr, text.length); 296 | for (let i = 0; i < words.length; ++i) words[i] = text.charCodeAt(i); 297 | return { 298 | ptr: ptr, 299 | length: words.length, 300 | free: function () { exports.free(ptr); } 301 | }; 302 | } 303 | 304 | /** 305 | * Create an object representing a Harfbuzz buffer. 306 | **/ 307 | function createBuffer() { 308 | var ptr = exports.hb_buffer_create(); 309 | return { 310 | ptr: ptr, 311 | /** 312 | * Add text to the buffer. 313 | * @param {string} text Text to be added to the buffer. 314 | **/ 315 | addText: function (text) { 316 | const str = createJsString(text); 317 | exports.hb_buffer_add_utf16(ptr, str.ptr, str.length, 0, str.length); 318 | str.free(); 319 | }, 320 | /** 321 | * Set buffer script, language and direction. 322 | * 323 | * This needs to be done before shaping. 324 | **/ 325 | guessSegmentProperties: function () { 326 | return exports.hb_buffer_guess_segment_properties(ptr); 327 | }, 328 | /** 329 | * Set buffer direction explicitly. 330 | * @param {string} direction: One of "ltr", "rtl", "ttb" or "btt" 331 | */ 332 | setDirection: function (dir) { 333 | exports.hb_buffer_set_direction(ptr, { 334 | ltr: 4, 335 | rtl: 5, 336 | ttb: 6, 337 | btt: 7 338 | }[dir] || 0); 339 | }, 340 | /** 341 | * Set buffer flags explicitly. 342 | * @param {string[]} flags: A list of strings which may be either: 343 | * "BOT" 344 | * "EOT" 345 | * "PRESERVE_DEFAULT_IGNORABLES" 346 | * "REMOVE_DEFAULT_IGNORABLES" 347 | * "DO_NOT_INSERT_DOTTED_CIRCLE" 348 | * "PRODUCE_UNSAFE_TO_CONCAT" 349 | */ 350 | setFlags: function (flags) { 351 | var flagValue = 0 352 | flags.forEach(function (s) { 353 | flagValue |= _buffer_flag(s); 354 | }) 355 | 356 | exports.hb_buffer_set_flags(ptr,flagValue); 357 | }, 358 | /** 359 | * Set buffer language explicitly. 360 | * @param {string} language: The buffer language 361 | */ 362 | setLanguage: function (language) { 363 | var str = createAsciiString(language); 364 | exports.hb_buffer_set_language(ptr, exports.hb_language_from_string(str.ptr,-1)); 365 | str.free(); 366 | }, 367 | /** 368 | * Set buffer script explicitly. 369 | * @param {string} script: The buffer script 370 | */ 371 | setScript: function (script) { 372 | var str = createAsciiString(script); 373 | exports.hb_buffer_set_script(ptr, exports.hb_script_from_string(str.ptr,-1)); 374 | str.free(); 375 | }, 376 | 377 | /** 378 | * Set the Harfbuzz clustering level. 379 | * 380 | * Affects the cluster values returned from shaping. 381 | * @param {number} level: Clustering level. See the Harfbuzz manual chapter 382 | * on Clusters. 383 | **/ 384 | setClusterLevel: function (level) { 385 | exports.hb_buffer_set_cluster_level(ptr, level) 386 | }, 387 | /** 388 | * Return the buffer contents as a JSON object. 389 | * 390 | * After shaping, this function will return an array of glyph information 391 | * objects. Each object will have the following attributes: 392 | * 393 | * - g: The glyph ID 394 | * - cl: The cluster ID 395 | * - ax: Advance width (width to advance after this glyph is painted) 396 | * - ay: Advance height (height to advance after this glyph is painted) 397 | * - dx: X displacement (adjustment in X dimension when painting this glyph) 398 | * - dy: Y displacement (adjustment in Y dimension when painting this glyph) 399 | * - flags: Glyph flags like `HB_GLYPH_FLAG_UNSAFE_TO_BREAK` (0x1) 400 | **/ 401 | json: function () { 402 | var length = exports.hb_buffer_get_length(ptr); 403 | var result = []; 404 | var infosPtr = exports.hb_buffer_get_glyph_infos(ptr, 0); 405 | var infosPtr32 = infosPtr / 4; 406 | var positionsPtr32 = exports.hb_buffer_get_glyph_positions(ptr, 0) / 4; 407 | var infos = Module.HEAPU32.subarray(infosPtr32, infosPtr32 + 5 * length); 408 | var positions = Module.HEAP32.subarray(positionsPtr32, positionsPtr32 + 5 * length); 409 | for (var i = 0; i < length; ++i) { 410 | result.push({ 411 | g: infos[i * 5 + 0], 412 | cl: infos[i * 5 + 2], 413 | ax: positions[i * 5 + 0], 414 | ay: positions[i * 5 + 1], 415 | dx: positions[i * 5 + 2], 416 | dy: positions[i * 5 + 3], 417 | flags: exports.hb_glyph_info_get_glyph_flags(infosPtr + i * 20) 418 | }); 419 | } 420 | return result; 421 | }, 422 | /** 423 | * Free the object. 424 | */ 425 | destroy: function () { exports.hb_buffer_destroy(ptr); } 426 | }; 427 | } 428 | 429 | /** 430 | * Shape a buffer with a given font. 431 | * 432 | * This returns nothing, but modifies the buffer. 433 | * 434 | * @param {object} font: A font returned from `createFont` 435 | * @param {object} buffer: A buffer returned from `createBuffer` and suitably 436 | * prepared. 437 | * @param {object} features: A string of comma-separated OpenType features to apply. 438 | */ 439 | function shape(font, buffer, features) { 440 | var featuresPtr = 0; 441 | var featuresLen = 0; 442 | if (features) { 443 | features = features.split(","); 444 | featuresPtr = exports.malloc(16 * features.length); 445 | features.forEach(function (feature, i) { 446 | var str = createAsciiString(feature); 447 | if (exports.hb_feature_from_string(str.ptr, -1, featuresPtr + featuresLen * 16)) 448 | featuresLen++; 449 | str.free(); 450 | }); 451 | } 452 | 453 | exports.hb_shape(font.ptr, buffer.ptr, featuresPtr, featuresLen); 454 | if (featuresPtr) 455 | exports.free(featuresPtr); 456 | } 457 | 458 | /** 459 | * Shape a buffer with a given font, returning a JSON trace of the shaping process. 460 | * 461 | * This function supports "partial shaping", where the shaping process is 462 | * terminated after a given lookup ID is reached. If the user requests the function 463 | * to terminate shaping after an ID in the GSUB phase, GPOS table lookups will be 464 | * processed as normal. 465 | * 466 | * @param {object} font: A font returned from `createFont` 467 | * @param {object} buffer: A buffer returned from `createBuffer` and suitably 468 | * prepared. 469 | * @param {object} features: A string of comma-separated OpenType features to apply. 470 | * @param {number} stop_at: A lookup ID at which to terminate shaping. 471 | * @param {number} stop_phase: Either 0 (don't terminate shaping), 1 (`stop_at` 472 | refers to a lookup ID in the GSUB table), 2 (`stop_at` refers to a lookup 473 | ID in the GPOS table). 474 | */ 475 | function shapeWithTrace(font, buffer, features, stop_at, stop_phase) { 476 | var trace = []; 477 | var currentPhase = DONT_STOP; 478 | var stopping = false; 479 | var failure = false; 480 | 481 | var traceBufLen = 1024 * 1024; 482 | var traceBufPtr = exports.malloc(traceBufLen); 483 | 484 | var traceFunc = function (bufferPtr, fontPtr, messagePtr, user_data) { 485 | var message = utf8Decoder.decode(Module.HEAPU8.subarray(messagePtr, Module.HEAPU8.indexOf(0, messagePtr))); 486 | if (message.startsWith("start table GSUB")) 487 | currentPhase = GSUB_PHASE; 488 | else if (message.startsWith("start table GPOS")) 489 | currentPhase = GPOS_PHASE; 490 | 491 | if (currentPhase != stop_phase) 492 | stopping = false; 493 | 494 | if (failure) 495 | return 1; 496 | 497 | if (stop_phase != DONT_STOP && currentPhase == stop_phase && message.startsWith("end lookup " + stop_at)) 498 | stopping = true; 499 | 500 | if (stopping) 501 | return 0; 502 | 503 | exports.hb_buffer_serialize_glyphs( 504 | bufferPtr, 505 | 0, exports.hb_buffer_get_length(bufferPtr), 506 | traceBufPtr, traceBufLen, 0, 507 | fontPtr, 508 | HB_BUFFER_SERIALIZE_FORMAT_JSON, 509 | HB_BUFFER_SERIALIZE_FLAG_NO_GLYPH_NAMES); 510 | 511 | trace.push({ 512 | m: message, 513 | t: JSON.parse(utf8Decoder.decode(Module.HEAPU8.subarray(traceBufPtr, Module.HEAPU8.indexOf(0, traceBufPtr)))), 514 | glyphs: exports.hb_buffer_get_content_type(bufferPtr) == HB_BUFFER_CONTENT_TYPE_GLYPHS, 515 | }); 516 | 517 | return 1; 518 | } 519 | 520 | var traceFuncPtr = addFunction(traceFunc, 'iiiii'); 521 | exports.hb_buffer_set_message_func(buffer.ptr, traceFuncPtr, 0, 0); 522 | shape(font, buffer, features, 0); 523 | exports.free(traceBufPtr); 524 | removeFunction(traceFuncPtr); 525 | 526 | return trace; 527 | } 528 | 529 | function version() { 530 | var versionPtr = exports.malloc(12); 531 | exports.hb_version(versionPtr, versionPtr + 4, versionPtr + 8); 532 | var version = { 533 | major: Module.HEAPU32[versionPtr / 4], 534 | minor: Module.HEAPU32[(versionPtr + 4) / 4], 535 | micro: Module.HEAPU32[(versionPtr + 8) / 4], 536 | }; 537 | exports.free(versionPtr); 538 | return version; 539 | } 540 | 541 | function version_string() { 542 | var versionPtr = exports.hb_version_string(); 543 | var version = utf8Decoder.decode(Module.HEAPU8.subarray(versionPtr, Module.HEAPU8.indexOf(0, versionPtr))); 544 | return version; 545 | } 546 | 547 | return { 548 | createBlob: createBlob, 549 | createFace: createFace, 550 | createFont: createFont, 551 | createBuffer: createBuffer, 552 | shape: shape, 553 | shapeWithTrace: shapeWithTrace, 554 | version: version, 555 | version_string: version_string, 556 | }; 557 | } 558 | 559 | // Should be replaced with something more reliable 560 | try { 561 | module.exports = hbjs; 562 | } catch (e) {} 563 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var hbjs = require('./hbjs.js'); 2 | var hb = require('./hb.js'); 3 | 4 | module.exports = new Promise(function (resolve, reject) { 5 | hb().then((instance) => { 6 | resolve(hbjs(instance)); 7 | }, reject); 8 | }); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harfbuzzjs", 3 | "version": "0.4.7", 4 | "description": "Minimal version of HarfBuzz for JavaScript use", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/index.js" 8 | }, 9 | "keywords": [ 10 | "harfbuzz", 11 | "opentype", 12 | "truetype", 13 | "ttf", 14 | "otf", 15 | "graphics", 16 | "complex scripts", 17 | "typography", 18 | "font rendering", 19 | "font", 20 | "fonts", 21 | "emoji" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/harfbuzz/harfbuzzjs.git" 26 | }, 27 | "author": "Ebrahim Byagowi ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/harfbuzz/harfbuzzjs/issues" 31 | }, 32 | "homepage": "https://github.com/harfbuzz/harfbuzzjs#readme", 33 | "devDependencies": { 34 | "chai": "^4.3.7", 35 | "mocha": "^10.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/fonts/noto/NotoSans-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harfbuzz/harfbuzzjs/0420d3838803eebb93d7ebea6ba85af2851d2c51/test/fonts/noto/NotoSans-Regular.otf -------------------------------------------------------------------------------- /test/fonts/noto/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harfbuzz/harfbuzzjs/0420d3838803eebb93d7ebea6ba85af2851d2c51/test/fonts/noto/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /test/fonts/noto/NotoSansArabic-Variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harfbuzz/harfbuzzjs/0420d3838803eebb93d7ebea6ba85af2851d2c51/test/fonts/noto/NotoSansArabic-Variable.ttf -------------------------------------------------------------------------------- /test/fonts/noto/NotoSansDevanagari-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harfbuzz/harfbuzzjs/0420d3838803eebb93d7ebea6ba85af2851d2c51/test/fonts/noto/NotoSansDevanagari-Regular.otf -------------------------------------------------------------------------------- /test/fonts/noto/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015-2021 Google LLC. All Rights Reserved. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { expect } = require('chai'); 4 | let hb; 5 | let blob, face, font, buffer; 6 | 7 | before(async function () { 8 | hb = await require('..'); 9 | }); 10 | 11 | afterEach(function () { 12 | if (blob) blob.destroy(); 13 | if (face) face.destroy(); 14 | if (font) font.destroy(); 15 | if (buffer) buffer.destroy(); 16 | blob = face = font = buffer = undefined; 17 | }); 18 | 19 | describe('Face', function () { 20 | it('collectUnicodes reflects codepoints supported by the font', function () { 21 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 22 | face = hb.createFace(blob); 23 | const codepoints = [...face.collectUnicodes()]; 24 | expect(codepoints).to.include('a'.codePointAt(0)); 25 | expect(codepoints).not.to.include('ا'.codePointAt(0)); 26 | }); 27 | 28 | it('exposes upem', function () { 29 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 30 | face = hb.createFace(blob); 31 | expect(face.upem).to.equal(1000); 32 | }); 33 | 34 | it('getAxisInfos returns details of a variable font', function () { 35 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSansArabic-Variable.ttf'))); 36 | face = hb.createFace(blob); 37 | expect(face.getAxisInfos()).to.deep.equal({ 38 | wght: { min: 100, default: 400, max: 900 }, 39 | wdth: { min: 62.5, default: 100, max: 100 } 40 | }); 41 | }); 42 | 43 | it('getAxisInfos returns an empty object for a non-variable font', function () { 44 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 45 | face = hb.createFace(blob); 46 | expect(Object.keys(face.getAxisInfos())).to.have.lengthOf(0); 47 | }); 48 | }); 49 | 50 | describe('Font', function () { 51 | it('glyphName returns names for glyph ids', function () { 52 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 53 | face = hb.createFace(blob); 54 | font = hb.createFont(face); 55 | expect(font.glyphName(20)).to.equal('one'); 56 | }); 57 | 58 | it('setScale affects advances', function () { 59 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 60 | face = hb.createFace(blob); 61 | font = hb.createFont(face); 62 | buffer = hb.createBuffer(); 63 | buffer.addText('a'); 64 | buffer.guessSegmentProperties(); 65 | font.setScale(1000 * 2, 1000 * 2); 66 | hb.shape(font, buffer) 67 | const glyphs = buffer.json(); 68 | expect(glyphs[0].ax).to.equal(561 * 2); 69 | }); 70 | 71 | it('setVariations affects advances', function () { 72 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSansArabic-Variable.ttf'))); 73 | face = hb.createFace(blob); 74 | font = hb.createFont(face); 75 | font.setVariations({ 'wght': 789 }); 76 | buffer = hb.createBuffer(); 77 | buffer.addText('آلو'); 78 | buffer.guessSegmentProperties(); 79 | hb.shape(font, buffer) 80 | const glyphs = buffer.json(); 81 | expect(glyphs[0].ax).to.equal(526); 82 | }); 83 | 84 | it('glyphToPath converts quadratic glyph to path', function () { 85 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 86 | face = hb.createFace(blob); 87 | font = hb.createFont(face); 88 | const expected21 = 'M520,0L48,0L48,73L235,262Q289,316 326,358Q363,400 382,440.5Q401,481 401,529Q401,\ 89 | 588 366,618.5Q331,649 275,649Q223,649 183.5,631Q144,613 103,581L56,640Q98,675 152.5,699.5Q207,724 275,\ 90 | 724Q375,724 433,673.5Q491,623 491,534Q491,478 468,429Q445,380 404,332.5Q363,285 308,231L159,84L159,80L520,80L520,0Z'; 91 | expect(font.glyphToPath(21)).to.equal(expected21); 92 | const expected22 = 'M493,547Q493,475 453,432.5Q413,390 345,376L345,372Q431,362 473,318Q515,274 515,203Q515,\ 93 | 141 486,92.5Q457,44 396.5,17Q336,-10 241,-10Q185,-10 137,-1.5Q89,7 45,29L45,111Q90,89 142,76.5Q194,64 242,64Q338,\ 94 | 64 380.5,101.5Q423,139 423,205Q423,272 370.5,301.5Q318,331 223,331L154,331L154,406L224,406Q312,406 357.5,443Q403,\ 95 | 480 403,541Q403,593 368,621.5Q333,650 273,650Q215,650 174,633Q133,616 93,590L49,650Q87,680 143.5,702Q200,724 272,\ 96 | 724Q384,724 438.5,674Q493,624 493,547Z'; 97 | expect(font.glyphToPath(22)).to.equal(expected22); 98 | }); 99 | 100 | it('glyphToPath converts cubic glyph to path', function () { 101 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.otf'))); 102 | face = hb.createFace(blob); 103 | font = hb.createFont(face); 104 | const expected21 = 'M520,0L520,80L159,80L159,84L308,231C418,338 491,422 491,534C491,652 408,724 275,724C184,724 112,\ 105 | 687 56,640L103,581C158,624 205,649 275,649C350,649 401,607 401,529C401,432 342,370 235,262L48,73L48,0L520,0Z'; 106 | expect(font.glyphToPath(21)).to.equal(expected21); 107 | const expected22 = 'M493,547C493,649 421,724 272,724C176,724 100,690 49,650L93,590C146,625 196,650 273,650C353,\ 108 | 650 403,610 403,541C403,460 341,406 224,406L154,406L154,331L223,331C349,331 423,294 423,205C423,117 370,64 242,64C178,\ 109 | 64 105,81 45,111L45,29C104,0 166,-10 241,-10C430,-10 515,78 515,203C515,297 459,358 345,372L345,376C435,394 493,451 493,547Z'; 110 | expect(font.glyphToPath(22)).to.equal(expected22); 111 | }); 112 | }); 113 | 114 | describe('Buffer', function () { 115 | it('setDirection controls direction of glyphs', function () { 116 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 117 | face = hb.createFace(blob); 118 | font = hb.createFont(face); 119 | buffer = hb.createBuffer(); 120 | buffer.addText('rtl'); 121 | buffer.setDirection('rtl'); 122 | hb.shape(font, buffer) 123 | const glyphs = buffer.json(); 124 | expect(glyphs[0].g).to.equal(79); // l 125 | expect(glyphs[1].g).to.equal(87); // t 126 | expect(glyphs[2].g).to.equal(85); // r 127 | }); 128 | 129 | it('setClusterLevel affects cluster merging', function () { 130 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 131 | face = hb.createFace(blob); 132 | font = hb.createFont(face); 133 | buffer = hb.createBuffer(); 134 | buffer.setClusterLevel(1); 135 | buffer.addText('x́'); 136 | buffer.guessSegmentProperties(); 137 | hb.shape(font, buffer) 138 | const glyphs = buffer.json(); 139 | expect(glyphs[0].cl).to.equal(0); 140 | expect(glyphs[1].cl).to.equal(1); 141 | }); 142 | 143 | it('setFlags with PRESERVE_DEFAULT_IGNORABLES affects glyph ids', function () { 144 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 145 | face = hb.createFace(blob); 146 | font = hb.createFont(face); 147 | buffer = hb.createBuffer(); 148 | buffer.addText('\u200dhi'); 149 | buffer.setFlags(['PRESERVE_DEFAULT_IGNORABLES']); 150 | buffer.guessSegmentProperties(); 151 | hb.shape(font, buffer) 152 | const glyphs = buffer.json(); 153 | expect(glyphs[0].g).not.to.equal(3 /* space */); 154 | }); 155 | }); 156 | 157 | describe('shape', function () { 158 | it('shape Latin string', function () { 159 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 160 | face = hb.createFace(blob); 161 | font = hb.createFont(face); 162 | buffer = hb.createBuffer(); 163 | buffer.addText('abc'); 164 | buffer.guessSegmentProperties(); 165 | hb.shape(font, buffer) 166 | const glyphs = buffer.json(); 167 | expect(glyphs[0]).to.deep.equal({ cl: 0, g: 68, ax: 561, ay: 0, dx: 0, dy: 0, flags: 0 } /* a */); 168 | expect(glyphs[1]).to.deep.equal({ cl: 1, g: 69, ax: 615, ay: 0, dx: 0, dy: 0, flags: 0 } /* b */); 169 | expect(glyphs[2]).to.deep.equal({ cl: 2, g: 70, ax: 480, ay: 0, dx: 0, dy: 0, flags: 0 } /* c */); 170 | }); 171 | 172 | it('shape Arabic string', function () { 173 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSansArabic-Variable.ttf'))); 174 | face = hb.createFace(blob); 175 | font = hb.createFont(face); 176 | buffer = hb.createBuffer(); 177 | buffer.addText('أبجد'); 178 | buffer.guessSegmentProperties(); 179 | hb.shape(font, buffer) 180 | const glyphs = buffer.json(); 181 | expect(glyphs[0]).to.deep.equal({ cl: 3, g: 213, ax: 532, ay: 0, dx: 0, dy: 0, flags: 1 } /* د */); 182 | expect(glyphs[1]).to.deep.equal({ cl: 2, g: 529, ax: 637, ay: 0, dx: 0, dy: 0, flags: 1 } /* ج */); 183 | expect(glyphs[2]).to.deep.equal({ cl: 1, g: 101, ax: 269, ay: 0, dx: 0, dy: 0, flags: 0 } /* ب */); 184 | expect(glyphs[3]).to.deep.equal({ cl: 0, g: 50, ax: 235, ay: 0, dx: 0, dy: 0, flags: 0 } /* أ */); 185 | }); 186 | 187 | it('shape with tracing', function () { 188 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 189 | face = hb.createFace(blob); 190 | font = hb.createFont(face); 191 | buffer = hb.createBuffer(); 192 | buffer.addText('abc'); 193 | buffer.guessSegmentProperties(); 194 | const result = hb.shapeWithTrace(font, buffer, "", 0, 0) 195 | expect(result).to.have.lengthOf(42); 196 | expect(result[0]).to.deep.equal({ 197 | "m": "start table GSUB script tag 'latn'", 198 | "glyphs": true, 199 | "t": [ 200 | { cl: 0, g: 68 }, 201 | { cl: 1, g: 69 }, 202 | { cl: 2, g: 70 }, 203 | ], 204 | }); 205 | expect(result[41]).to.deep.equal({ 206 | "m": "end table GPOS script tag 'latn'", 207 | "glyphs": true, 208 | "t": [ 209 | { cl: 0, g: 68, ax: 561, ay: 0, dx: 0, dy: 0 }, 210 | { cl: 1, g: 69, ax: 615, ay: 0, dx: 0, dy: 0 }, 211 | { cl: 2, g: 70, ax: 480, ay: 0, dx: 0, dy: 0 }, 212 | ], 213 | }); 214 | }); 215 | 216 | it('shape with tracing and features', function () { 217 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSans-Regular.ttf'))); 218 | face = hb.createFace(blob); 219 | font = hb.createFont(face); 220 | buffer = hb.createBuffer(); 221 | buffer.addText('fi AV'); 222 | buffer.guessSegmentProperties(); 223 | const result = hb.shapeWithTrace(font, buffer, "-liga,-kern", 0, 0) 224 | expect(result).to.have.lengthOf(29); 225 | expect(result[0]).to.deep.equal({ 226 | "m": "start table GSUB script tag 'latn'", 227 | "glyphs": true, 228 | "t": [ 229 | { cl: 0, g: 73 }, 230 | { cl: 1, g: 76 }, 231 | { cl: 2, g: 3 }, 232 | { cl: 3, g: 36 }, 233 | { cl: 4, g: 57 }, 234 | ], 235 | }); 236 | expect(result[28]).to.deep.equal({ 237 | "m": "end table GPOS script tag 'latn'", 238 | "glyphs": true, 239 | "t": [ 240 | { cl: 0, g: 73, ax: 344, ay: 0, dx: 0, dy: 0 }, 241 | { cl: 1, g: 76, ax: 258, ay: 0, dx: 0, dy: 0 }, 242 | { cl: 2, g: 3, ax: 260, ay: 0, dx: 0, dy: 0 }, 243 | { cl: 3, g: 36, ax: 639, ay: 0, dx: 0, dy: 0 }, 244 | { cl: 4, g: 57, ax: 600, ay: 0, dx: 0, dy: 0 }, 245 | ], 246 | }); 247 | }); 248 | 249 | it('shape with 3-letter languae tag', function () { 250 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSansDevanagari-Regular.otf'))); 251 | face = hb.createFace(blob); 252 | font = hb.createFont(face); 253 | buffer = hb.createBuffer(); 254 | buffer.addText('५ल'); 255 | buffer.guessSegmentProperties(); 256 | hb.shape(font, buffer) 257 | var glyphs = buffer.json(); 258 | expect(glyphs).to.have.lengthOf(2); 259 | expect(glyphs[0].g).to.equal(118); 260 | buffer.destroy(); 261 | 262 | buffer = hb.createBuffer(); 263 | buffer.addText('५ल'); 264 | buffer.setLanguage('dty'); 265 | buffer.guessSegmentProperties(); 266 | hb.shape(font, buffer) 267 | var glyphs = buffer.json(); 268 | expect(glyphs).to.have.lengthOf(2); 269 | expect(glyphs[0].g).to.equal(123); 270 | }); 271 | 272 | it('shape with OpenType language tag', function () { 273 | blob = hb.createBlob(fs.readFileSync(path.join(__dirname, 'fonts/noto/NotoSansDevanagari-Regular.otf'))); 274 | face = hb.createFace(blob); 275 | font = hb.createFont(face); 276 | buffer = hb.createBuffer(); 277 | buffer.addText('५ल'); 278 | buffer.guessSegmentProperties(); 279 | hb.shape(font, buffer) 280 | var glyphs = buffer.json(); 281 | expect(glyphs).to.have.lengthOf(2); 282 | expect(glyphs[0].g).to.equal(118); 283 | buffer.destroy(); 284 | 285 | buffer = hb.createBuffer(); 286 | buffer.addText('५ल'); 287 | buffer.setLanguage('x-hbot-4e455020'); // 'NEP ' 288 | buffer.guessSegmentProperties(); 289 | hb.shape(font, buffer) 290 | var glyphs = buffer.json(); 291 | expect(glyphs).to.have.lengthOf(2); 292 | expect(glyphs[0].g).to.equal(123); 293 | }); 294 | }); 295 | 296 | describe('misc', function () { 297 | it('get version', function () { 298 | const version = hb.version(); 299 | expect(version).to.have.property('major').that.is.a('number'); 300 | expect(version).to.have.property('minor').that.is.a('number'); 301 | expect(version).to.have.property('micro').that.is.a('number'); 302 | expect(version.major).to.be.at.least(10); 303 | }); 304 | it('get version string', function () { 305 | const version_string = hb.version_string(); 306 | expect(version_string).to.match(/^\d+\.\d+\.\d+$/); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "es6", 5 | "target": "es6", 6 | "sourceMap": true, 7 | "alwaysStrict": true, 8 | "noImplicitThis": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "strictNullChecks": true, 12 | "skipLibCheck": true, 13 | "strictPropertyInitialization": true 14 | }, 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } --------------------------------------------------------------------------------