unicode-shaper
Version:
Shape unicode text so that renderers like WebGL and WebGPU can properly display the glyphs.
150 lines (133 loc) • 4.49 kB
text/typescript
import { DEFAULT_OPTIONS } from './shape/internal.js';
import wasmBase64 from './uShaper.wasm.js';
/** WASM Free Command */
type WasmFree = (ptr: number, len: number) => void;
/** WASM Process Text Command */
type WasmProcessText = (ptr: number, len: number, options: number) => void;
/** WASM Alloc Sentinel Command */
type WasmAllocSentinel = (size: number) => number;
/** WASM Is Right-to-Left Command */
type WasmIsRTL = (unicode: number) => number;
/** WASM Is Chinese-Japanese-Korean Command */
type WasmIsCJK = (unicode: number) => number;
/**
* WASM based text shaper
*/
export class WasmTextShaper {
instance!: WebAssembly.Instance;
wasmMemory?: Uint8Array;
tmpString = '';
/** Construct the WASM instance */
constructor() {
const mod = new WebAssembly.Module(base64ToArrayBuffer(wasmBase64));
this.instance = new WebAssembly.Instance(mod, {
env: {
/**
* Set a unicode array
* @param ptr - pointer
* @param len - length
*/
setUnicodeArray: (ptr: number, len: number): void => {
this.tmpString = '';
if (len === 0 || ptr < 0) return;
const buf = this.#get(ptr, len);
for (let i = 0; i < len; i++) this.tmpString += String.fromCharCode(buf[i]);
},
},
});
}
/**
* Converts a string into a shaped string
* @param str - input string
* @param options - shaping options
* @returns - shaped string
*/
shapeString(str: string, options = DEFAULT_OPTIONS): string {
const processText = this.instance.exports.processText as WasmProcessText;
const free = this.instance.exports.free as WasmFree;
if (str.length === 0) return str;
const len = str.length;
// NOTE: putString allocates memory, but processText will free it for us
const ptr = this.#putString(str);
processText(ptr, len, options);
free(ptr, len);
return this.tmpString;
}
/**
* Check if a character is a "right-to-left" unicode character
* @param unicode - input unicode character
* @returns - True if right-to-left
*/
isRTL(unicode: number): boolean {
const isRTL = this.instance.exports.isRTL as WasmIsRTL;
return isRTL(unicode) === 1;
}
/**
* Check if a character is CJK (Chinese, Japanese, or Korean)
* @param unicode - input unicode character
* @returns - True if CJK
*/
isCJK(unicode: number): boolean {
const isCJK = this.instance.exports.isCJK as WasmIsCJK;
return isCJK(unicode) === 1;
}
/**
* Converts a string into a unicode array inside the WASM memory. Returns the pointer
* @param str - input string
* @returns - pointer
*/
#putString(str: string): number {
const len = str.length;
const buf = new Uint16Array(len);
const ptr = this.#allocUnicodeArray(len);
for (let i = 0; i < len; i++) buf[i] = str.charCodeAt(i);
const view = this.#getMemory();
view.subarray(ptr, ptr + len * 2).set(new Uint8Array(buf.buffer));
return ptr;
}
/**
* Reads a unicode array from the WASM memory
* @param ptr - pointer
* @param len - length
* @returns - unicode array
*/
#get(ptr: number, len: number): Uint16Array {
const view = this.#getMemory();
const view16 = new Uint16Array(view.buffer, ptr, len);
const copy = new Uint16Array(len);
for (let i = 0; i < len; i++) copy[i] = view16[i];
return copy;
}
/**
* Allocates a unicode array
* @param size - size of array
* @returns - pointer to array memory
*/
#allocUnicodeArray(size: number): number {
const allocUnicodeArray = this.instance.exports.allocUnicodeArray as WasmAllocSentinel;
return allocUnicodeArray(size);
}
/**
* Returns the WASM memory. Rebuilds pointer if needed
* @returns - WASM memory
*/
#getMemory(): Uint8Array {
const memory = this.instance.exports.memory as WebAssembly.Memory;
if (this.wasmMemory === undefined || this.wasmMemory.buffer !== memory.buffer) {
this.wasmMemory = new Uint8Array(memory.buffer);
}
return this.wasmMemory;
}
}
/**
* polyfill to convert wasm base64 binary to ArrayBuffer
* @param base64 - input base64 string
* @returns - ArrayBuffer
*/
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
return bytes.buffer as ArrayBuffer;
}