├── .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 | }
--------------------------------------------------------------------------------