harfbuzzjs
Version:
Minimal version of HarfBuzz for JavaScript use
289 lines (249 loc) • 9.37 kB
text/typescript
type Pointer = number;
const HB_MEMORY_MODE_WRITABLE: number = 2;
const HB_SET_VALUE_INVALID: Pointer = -1;
class HarfBuzzExports {
readonly heapu8: Uint8Array;
readonly heapu32: Uint32Array;
readonly heapi32: Int32Array;
readonly utf8Encoder: TextEncoder;
//exported HarfBuzz methods
readonly malloc: (length: number) => Pointer
readonly free: (ptr: Pointer) => void
readonly free_ptr: () => Pointer
readonly hb_blob_create: (data: Pointer, length: number, memoryMode: number, useData: Pointer, destroyFunction: Pointer) => Pointer
readonly hb_blob_destroy: (ptr: Pointer) => void
readonly hb_face_create: (blobPtr: Pointer, index: number) => Pointer
readonly hb_face_get_upem: (facePtr: Pointer) => number
readonly hb_face_destroy: (ptr: Pointer) => void
readonly hb_font_create: (facePtr: Pointer) => Pointer
readonly hb_font_set_scale: (fontPtr: Pointer, xScale: number, yScale: number) => void
readonly hb_font_destroy: (ptr: Pointer) => void
readonly hb_face_collect_unicodes: (facePtr: Pointer, setPtr: Pointer) => void
readonly hb_set_create: () => Pointer
readonly hb_set_destroy: (setPtr: Pointer) => void
readonly hb_set_get_population: (setPtr: Pointer) => number
readonly hb_set_next_many: (
setPtr: Pointer,
greaterThanUnicodePtr: Pointer,
outputU32ArrayPtr: Pointer,
size: number,
) => number
readonly hb_buffer_create: () => Pointer
readonly hb_buffer_add_utf8: (bufferPtr: Pointer, stringPtr: Pointer, stringLength: number, itemOffset: number, itemLength: number) => void
readonly hb_buffer_guess_segment_properties: (bufferPtr: Pointer) => void
readonly hb_buffer_set_direction: (bufferPtr: Pointer, direction: number) => void
readonly hb_shape: (fontPtr: Pointer, bufferPtr: Pointer, features: any, numFeatures: number) => void
readonly hb_buffer_get_length: (bufferPtr: Pointer) => number
readonly hb_buffer_get_glyph_infos: (bufferPtr: Pointer, length: number) => any
readonly hb_buffer_get_glyph_positions: (bufferPtr: Pointer, length: number) => any
readonly hb_buffer_destroy: (bufferPtr: Pointer) => void
constructor(exports: any) {
this.heapu8 = new Uint8Array(exports.memory.buffer);
this.heapu32 = new Uint32Array(exports.memory.buffer);
this.heapi32 = new Int32Array(exports.memory.buffer);
this.utf8Encoder = new TextEncoder();
this.malloc = exports.malloc;
this.free = exports.free;
this.free_ptr = exports.free_ptr;
this.hb_blob_destroy = exports.hb_blob_destroy;
this.hb_blob_create = exports.hb_blob_create;
this.hb_face_create = exports.hb_face_create;
this.hb_face_get_upem = exports.hb_face_get_upem;
this.hb_face_destroy = exports.hb_face_destroy;
this.hb_face_collect_unicodes = exports.hb_face_collect_unicodes;
this.hb_set_create = exports.hb_set_create;
this.hb_set_destroy = exports.hb_set_destroy;
this.hb_set_get_population = exports.hb_set_get_population;
this.hb_set_next_many = exports.hb_set_next_many;
this.hb_font_create = exports.hb_font_create;
this.hb_font_set_scale = exports.hb_font_set_scale;
this.hb_font_destroy = exports.hb_font_destroy;
this.hb_buffer_create = exports.hb_buffer_create;
this.hb_buffer_add_utf8 = exports.hb_buffer_add_utf8;
this.hb_buffer_guess_segment_properties = exports.hb_buffer_guess_segment_properties;
this.hb_buffer_set_direction = exports.hb_buffer_set_direction;
this.hb_shape = exports.hb_shape;
this.hb_buffer_get_length = exports.hb_buffer_get_length;
this.hb_buffer_get_glyph_infos = exports.hb_buffer_get_glyph_infos;
this.hb_buffer_get_glyph_positions = exports.hb_buffer_get_glyph_positions;
this.hb_buffer_destroy = exports.hb_buffer_destroy;
}
}
let hb: HarfBuzzExports;
class CString {
readonly ptr: Pointer;
readonly length: number;
constructor(text: string) {
var bytes = hb.utf8Encoder.encode(text);
this.ptr = hb.malloc(bytes.byteLength);
hb.heapu8.set(bytes, this.ptr);
this.length = bytes.byteLength;
}
destroy() {
hb.free(this.ptr);
}
}
export class HarfBuzzBlob {
readonly ptr: Pointer;
constructor(data: Uint8Array) {
let blobPtr = hb.malloc(data.length);
hb.heapu8.set(data, blobPtr);
this.ptr = hb.hb_blob_create(blobPtr, data.byteLength, HB_MEMORY_MODE_WRITABLE, blobPtr, hb.free_ptr());
}
destroy() {
hb.hb_blob_destroy(this.ptr);
}
}
function typedArrayFromSet<T extends 'u8' | 'u32' | 'i32'>(setPtr: Pointer, arrayType: T) {
const heap = hb[`heap${arrayType}`];
const bytesPerElment = heap.BYTES_PER_ELEMENT;
const setCount = hb.hb_set_get_population(setPtr);
const arrayPtr = hb.malloc(
setCount * bytesPerElment,
);
const arrayOffset = arrayPtr / bytesPerElment;
const array = heap.subarray(
arrayOffset,
arrayOffset + setCount,
) as typeof hb[`heap${T}`];
heap.set(array, arrayOffset);
hb.hb_set_next_many(
setPtr,
HB_SET_VALUE_INVALID,
arrayPtr,
setCount,
);
return array;
}
export class HarfBuzzFace {
readonly ptr: Pointer;
constructor(blob: HarfBuzzBlob, index: number) {
this.ptr = hb.hb_face_create(blob.ptr, index);
}
getUnitsPerEM() {
return hb.hb_face_get_upem(this.ptr);
}
collectUnicodes() {
const unicodeSetPtr = hb.hb_set_create();
hb.hb_face_collect_unicodes(this.ptr, unicodeSetPtr);
const result = typedArrayFromSet(unicodeSetPtr, 'u32');
hb.hb_set_destroy(unicodeSetPtr);
return result;
}
destroy() {
hb.hb_face_destroy(this.ptr);
}
}
export class HarfBuzzFont {
readonly ptr: Pointer
readonly unitsPerEM: number
constructor(face: HarfBuzzFace) {
this.ptr = hb.hb_font_create(face.ptr);
this.unitsPerEM = face.getUnitsPerEM();
}
setScale(xScale: number, yScale: number) {
hb.hb_font_set_scale(this.ptr, xScale, yScale);
}
destroy() {
hb.hb_font_destroy(this.ptr);
}
}
export type HarfBuzzDirection = "ltr" | "rtl" | "ttb" | "btt"
class GlyphInformation {
readonly GlyphId: number
readonly Cluster: number
readonly XAdvance: number
readonly YAdvance: number
readonly XOffset: number
readonly YOffset: number
constructor(glyphId: number, cluster: number, xAdvance: number, yAdvance: number, xOffset: number, yOffset: number) {
this.GlyphId = glyphId;
this.Cluster = cluster;
this.XAdvance = xAdvance;
this.YAdvance = yAdvance;
this.XOffset = xOffset;
this.YOffset = yOffset;
}
}
export class HarfBuzzBuffer {
readonly ptr: Pointer
constructor() {
this.ptr = hb.hb_buffer_create();
}
addText(text: string) {
let str = new CString(text);
hb.hb_buffer_add_utf8(this.ptr, str.ptr, str.length, 0, str.length);
str.destroy();
}
guessSegmentProperties() {
hb.hb_buffer_guess_segment_properties(this.ptr);
}
setDirection(direction: HarfBuzzDirection) {
let d = { "ltr": 4, "rtl": 5, "ttb": 6, "btt": 7 }[direction];
hb.hb_buffer_set_direction(this.ptr, d);
}
json() {
var length = hb.hb_buffer_get_length(this.ptr);
var result = new Array<GlyphInformation>();
var infosPtr32 = hb.hb_buffer_get_glyph_infos(this.ptr, 0) / 4;
var positionsPtr32 = hb.hb_buffer_get_glyph_positions(this.ptr, 0) / 4;
var infos = hb.heapu32.subarray(infosPtr32, infosPtr32 + 5 * length);
var positions = hb.heapi32.subarray(positionsPtr32, positionsPtr32 + 5 * length);
for (var i = 0; i < length; ++i) {
result.push(new GlyphInformation(
infos[i * 5 + 0],
infos[i * 5 + 2],
positions[i * 5 + 0],
positions[i * 5 + 1],
positions[i * 5 + 2],
positions[i * 5 + 3]));
}
return result;
}
destroy() {
hb.hb_buffer_destroy(this.ptr)
}
}
export function shape(text: string, font: HarfBuzzFont, features: any): Array<GlyphInformation> {
let buffer = new HarfBuzzBuffer();
buffer.addText(text);
buffer.guessSegmentProperties();
buffer.shape(font, features);
let result = buffer.json();
buffer.destroy();
return result;
}
export function getWidth(text: string, font: HarfBuzzFont, fontSizeInPixel: number, features: any): number {
let scale = fontSizeInPixel / font.unitsPerEM;
let shapeResult = shape(text, font, features);
let totalWidth = shapeResult.map((glyphInformation) => {
return glyphInformation.XAdvance;
}).reduce((previous, current, i, arr) => {
return previous + current;
}, 0.0);
return totalWidth * scale;
}
export const harfbuzzFonts = new Map<string, HarfBuzzFont>();
export function loadHarfbuzz(webAssemblyUrl: string): Promise<void> {
return fetch(webAssemblyUrl).then(response => {
return response.arrayBuffer();
}).then(wasm => {
return WebAssembly.instantiate(wasm);
}).then(result => {
//@ts-ignore
hb = new HarfBuzzExports(result.instance.exports);
});
}
export function loadAndCacheFont(fontName: string, fontUrl: string): Promise<void> {
return fetch(fontUrl).then((response) => {
return response.arrayBuffer().then((blob) => {
let fontBlob = new Uint8Array(blob);
let harfbuzzBlob = new HarfBuzzBlob(fontBlob);
let harfbuzzFace = new HarfBuzzFace(harfbuzzBlob, 0);
let harfbuzzFont = new HarfBuzzFont(harfbuzzFace);
harfbuzzFonts.set(fontName, harfbuzzFont);
harfbuzzFace.destroy();
harfbuzzBlob.destroy();
});
});
}