UNPKG

dropflow

Version:

A small CSS2 document renderer built from specifications

463 lines (462 loc) 16.9 kB
import wasm from './wasm.js'; import { setCtx, onWasmMemoryResized } from './wasm-env.js'; const exports = wasm.instance.exports; let heapu8 = new Uint8Array(exports.memory.buffer); let heapu32 = new Uint32Array(exports.memory.buffer); let heapi32 = new Int32Array(exports.memory.buffer); let heapf32 = new Float32Array(exports.memory.buffer); onWasmMemoryResized(() => { heapu8 = new Uint8Array(exports.memory.buffer); heapu32 = new Uint32Array(exports.memory.buffer); heapi32 = new Int32Array(exports.memory.buffer); heapf32 = new Float32Array(exports.memory.buffer); }); const utf8Decoder = new TextDecoder('utf8'); const utf16Decoder = new TextDecoder('utf-16'); const HB_MEMORY_MODE_WRITABLE = 2; const bytes4 = exports.malloc(4); export function hb_tag(s) { return ((s.charCodeAt(0) & 0xFF) << 24 | (s.charCodeAt(1) & 0xFF) << 16 | (s.charCodeAt(2) & 0xFF) << 8 | (s.charCodeAt(3) & 0xFF) << 0); } export function _hb_untag(tag) { return [ String.fromCharCode((tag >> 24) & 0xFF), String.fromCharCode((tag >> 16) & 0xFF), String.fromCharCode((tag >> 8) & 0xFF), String.fromCharCode((tag >> 0) & 0xFF) ].join(''); } export const HB_BUFFER_FLAG_BOT = 0x1; export const HB_BUFFER_FLAG_EOT = 0x2; export const HB_BUFFER_FLAG_PRESERVE_DEFAULT_IGNORABLES = 0x4; export const HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES = 0x8; export const HB_BUFFER_FLAG_DO_NOT_INSERT_DOTTED_CIRCLE = 0x10; export const HB_BUFFER_FLAG_PRODUCE_UNSAFE_TO_CONCAT = 0x40; export class HbSet { ptr; constructor(ptr) { this.ptr = ptr; } add(codepoint) { return exports.hb_set_add(this.ptr, codepoint); } addRange(start, end) { exports.hb_set_add_range(this.ptr, start, end); } has(value) { return exports.hb_set_has(this.ptr, value); } union(set) { exports.hb_set_union(this.ptr, set.ptr); } copy() { return createSetInternal(exports.hb_set_copy(this.ptr)); } subtract(set) { exports.hb_set_subtract(this.ptr, set.ptr); } getPopulation() { return exports.hb_set_get_population(this.ptr); } clear() { exports.hb_set_clear(this.ptr); } destroy() { exports.hb_set_destroy(this.ptr); } [Symbol.iterator]() { const valuePtr = exports.malloc(4); heapu32[valuePtr >>> 2] = -1; const next = () => { if (exports.hb_set_next(this.ptr, valuePtr)) { return { value: heapu32[valuePtr >>> 2], done: false }; } else { return { done: true }; } }; const return_ = (value) => { exports.free(valuePtr); return { value, done: true }; }; return { next, return: return_ }; } } function createSetInternal(uptr = 0) { return new HbSet(uptr || exports.hb_set_create()); } export function createSet() { return createSetInternal(); } export function wrapExternalSet(ptr) { return createSetInternal(ptr); } export class HbBlob { ptr; constructor(ptr) { this.ptr = ptr; } destroy() { exports.hb_blob_destroy(this.ptr); } countFaces() { return exports.hb_face_count(this.ptr); } getData() { const length = exports.hb_blob_get_length(this.ptr); const ptr = exports.hb_blob_get_data(this.ptr, 0); return heapu8.subarray(ptr, ptr + length); } } export function createBlob(blob) { const blobPtr = exports.malloc(blob.byteLength); heapu8.set(blob, blobPtr); return new HbBlob(exports.hb_blob_create(blobPtr, blob.byteLength, HB_MEMORY_MODE_WRITABLE, blobPtr, exports.free_ptr())); } const fontNameBufferSize = 2048; const fontNameBuffer = exports.malloc(fontNameBufferSize); // permanently allocated function createAsciiString(text) { var ptr = exports.malloc(text.length + 1); for (let i = 0; i < text.length; ++i) { const char = text.charCodeAt(i); if (char > 127) throw new Error('Expected ASCII text'); heapu8[ptr + i] = char; } heapu8[ptr + text.length] = 0; return { ptr: ptr, length: text.length, free: function () { exports.free(ptr); } }; } export const HB_OT_TAG_GSUB = hb_tag('GSUB'); export const HB_OT_TAG_GPOS = hb_tag('GPOS'); export const HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX = 0xffff; export class HbFace { ptr; upem; constructor(ptr) { this.ptr = ptr; this.upem = exports.hb_face_get_upem(ptr); } getAxisInfos() { const axis = exports.malloc(64 * 32); const c = exports.malloc(4); heapu32[c / 4] = 64; exports.hb_ot_var_get_axis_infos(this.ptr, 0, c, axis); const result = {}; Array.from({ length: heapu32[c / 4] }).forEach(function (_, i) { result[_hb_untag(heapu32[axis / 4 + i * 8 + 1])] = { min: heapf32[axis / 4 + i * 8 + 4], default: heapf32[axis / 4 + i * 8 + 5], max: heapf32[axis / 4 + i * 8 + 6] }; }); exports.free(c); exports.free(axis); return result; } collectUnicodes() { const setPtr = exports.hb_set_create(); exports.hb_face_collect_unicodes(this.ptr, setPtr); return createSetInternal(setPtr); } getName(nameId, language) { const writtenPtr = exports.malloc(4); const written = new Uint32Array(exports.memory.buffer, writtenPtr, 1); const cLanguage = createAsciiString(language); const hbLanguage = exports.hb_language_from_string(cLanguage, -1); cLanguage.free(); written[0] = fontNameBufferSize; exports.hb_ot_name_get_utf16(this.ptr, nameId, hbLanguage, writtenPtr, fontNameBuffer); const str = utf16Decoder.decode(new Uint16Array(exports.memory.buffer, fontNameBuffer, written[0])); exports.free(writtenPtr); return str; } hasSubstitution() { return exports.hb_ot_layout_has_substitution(this.ptr); } hasPositioning() { return exports.hb_ot_layout_has_positioning(this.ptr); } referenceTable(tag) { return new HbBlob(exports.hb_face_reference_table(this.ptr, hb_tag(tag))); } getScripts() { const lengthPtr = bytes4; const maxLength = 8; const tagsPtr = exports.malloc(maxLength * 4); const tags = []; let offset = 0; let length; heapu32[lengthPtr >> 2] = maxLength; do { exports.hb_ot_layout_table_get_script_tags(this.ptr, HB_OT_TAG_GSUB, offset, lengthPtr, tagsPtr); length = heapu32[lengthPtr >> 2]; for (let i = 0; i < length; i++) { tags.push(heapu32[(tagsPtr >> 2) + i]); } offset += length; } while (length === maxLength); exports.free(tagsPtr); return tags; } getNumLangsForScript(table, scriptIndex) { return exports.hb_ot_layout_script_get_language_tags(this.ptr, table, scriptIndex, 0, 0, 0); } getFeatureIndexes(table, scriptIndex, langIndex) { const lengthPtr = bytes4; const maxLength = 32; const featureIndexesPtr = exports.malloc(maxLength * 4); const indexes = []; let offset = 0; let length; heapu32[lengthPtr >> 2] = maxLength; do { exports.hb_ot_layout_language_get_feature_indexes(this.ptr, table, scriptIndex, langIndex, offset, lengthPtr, featureIndexesPtr); length = heapu32[lengthPtr >> 2]; for (let i = 0; i < length; i++) { indexes.push(heapu32[(featureIndexesPtr >> 2) + i]); } offset += length; } while (length === maxLength); exports.free(featureIndexesPtr); return indexes; } getRequiredFeatureIndex(table, scriptIndex, langIndex) { const featurePtr = bytes4; if (exports.hb_ot_layout_language_get_required_feature_index(this.ptr, table, scriptIndex, langIndex, featurePtr)) { return heapu32[featurePtr >> 2]; } else { return -1; } } getFeatureTags(table, scriptIndex, langIndex) { const lengthPtr = bytes4; const maxLength = 32; const featureTagsPtr = exports.malloc(maxLength * 4); const tags = []; let offset = 0; let length; heapu32[lengthPtr >> 2] = maxLength; do { exports.hb_ot_layout_language_get_feature_tags(this.ptr, table, scriptIndex, langIndex, offset, lengthPtr, featureTagsPtr); length = heapu32[lengthPtr >> 2]; for (let i = 0; i < length; i++) { tags.push(heapu32[(featureTagsPtr >> 2) + i]); } offset += length; } while (length === maxLength); exports.free(featureTagsPtr); return tags; } getLookupsByFeature(table, featureIndex, lookups) { const lengthPtr = bytes4; const maxLength = 32; const lookupsPtr = exports.malloc(maxLength * 4); let offset = 0; let length; heapu32[lengthPtr >> 2] = maxLength; do { exports.hb_ot_layout_feature_get_lookups(this.ptr, table, featureIndex, offset, lengthPtr, lookupsPtr); length = heapu32[lengthPtr >> 2]; for (let i = 0; i < length; i++) { lookups.add(heapu32[(lookupsPtr >> 2) + i]); } offset += length; } while (length === maxLength); exports.free(lookupsPtr); } collectGlyphs(table, lookupIndex, beforeGlyphs, inputGlyphs, afterGlyphs, outputGlyphs) { exports.hb_ot_layout_lookup_collect_glyphs(this.ptr, table, lookupIndex, beforeGlyphs?.ptr ?? 0, inputGlyphs?.ptr ?? 0, afterGlyphs?.ptr ?? 0, outputGlyphs?.ptr ?? 0); } referenceBlob() { return new HbBlob(exports.hb_face_reference_blob(this.ptr)); } destroy() { exports.hb_face_destroy(this.ptr); } } export function createFace(blob, index) { return new HbFace(exports.hb_face_create(blob.ptr, index)); } const nameBufferSize = 256; // should be enough for most glyphs const nameBuffer = exports.malloc(nameBufferSize); // permanently allocated export class HbFont { ptr; constructor(ptr) { this.ptr = ptr; } glyphName(glyphId) { exports.hb_font_glyph_to_string(this.ptr, glyphId, nameBuffer, nameBufferSize); const array = heapu8.subarray(nameBuffer, nameBuffer + nameBufferSize); return utf8Decoder.decode(array.slice(0, array.indexOf(0))); } getNominalGlyph(codepoint) { exports.hb_font_get_nominal_glyph(this.ptr, codepoint, bytes4); return heapu32[bytes4 >>> 2]; } drawGlyph(glyphId, ctx) { setCtx(ctx); exports.hbjs_glyph_draw(this.ptr, glyphId); } getStyle(styleTag) { return exports.hb_style_get_value(this.ptr, hb_tag(styleTag)); } setScale(xScale, yScale) { exports.hb_font_set_scale(this.ptr, xScale, yScale); } setVariations(variations) { const entries = Object.entries(variations); const vars = exports.malloc(8 * entries.length); entries.forEach(function (entry, i) { heapu32[vars / 4 + i * 2 + 0] = hb_tag(entry[0]); heapf32[vars / 4 + i * 2 + 1] = entry[1]; }); exports.hb_font_set_variations(this.ptr, vars, entries.length); exports.free(vars); } getMetrics(dir) { const extentsPtr = exports.malloc(4); // i32 * 12 const extentsOffset = extentsPtr / 4; let ascender, descender, lineGap; if (dir === 'ltr' || dir === 'rtl') { exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('hasc'), extentsPtr); ascender = heapi32[extentsOffset]; exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('hdsc'), extentsPtr); descender = heapi32[extentsOffset]; exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('hlgp'), extentsPtr); lineGap = heapi32[extentsOffset]; } else { exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('vasc'), extentsPtr); ascender = heapi32[extentsOffset]; exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('vdsc'), extentsPtr); descender = heapi32[extentsOffset]; exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('vlgp'), extentsPtr); lineGap = heapi32[extentsOffset]; } exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('spyo'), extentsPtr); const superscript = heapi32[extentsOffset]; exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('sbyo'), extentsPtr); const subscript = heapi32[extentsOffset]; exports.hb_ot_metrics_get_position_with_fallback(this.ptr, hb_tag('xhgt'), extentsPtr); const xHeight = heapi32[extentsOffset]; exports.free(extentsPtr); return { ascender, descender, lineGap, superscript, subscript, xHeight }; } destroy() { exports.hb_font_destroy(this.ptr); } } export function createFont(face) { return new HbFont(exports.hb_font_create(face.ptr)); } function createJsString(text) { const ptr = exports.malloc(text.length * 2); const words = new Uint16Array(exports.memory.buffer, ptr, text.length); for (let i = 0; i < words.length; ++i) words[i] = text.charCodeAt(i); return { ptr: ptr, length: words.length, free: function () { exports.free(ptr); } }; } const langPtr = exports.malloc(3); // hbjs_extract_glyphs export const G_ID = 0; export const G_CL = 1; export const G_AX = 2; export const G_AY = 3; export const G_DX = 4; export const G_DY = 5; export const G_FL = 6; export const G_SZ = 7; export class HbBuffer { ptr; constructor(ptr) { this.ptr = ptr; } getLength() { return exports.hb_buffer_get_length(this.ptr); } setLength(length) { exports.hb_buffer_set_length(this.ptr, length); } addText(text) { const str = createJsString(text); exports.hb_buffer_add_utf16(this.ptr, str.ptr, str.length, 0, str.length); str.free(); } addUtf16(paragraphPtr, paragraphLength, offset, length) { exports.hb_buffer_add_utf16(this.ptr, paragraphPtr, paragraphLength, offset, length); } guessSegmentProperties() { exports.hb_buffer_guess_segment_properties(this.ptr); } setDirection(dir) { exports.hb_buffer_set_direction(this.ptr, { ltr: 4, rtl: 5, ttb: 6, btt: 7 }[dir] || 0); } setFlags(flags) { exports.hb_buffer_set_flags(this.ptr, flags); } setLanguage(language) { const len = Math.min(3, language.length); for (let i = 0; i < len; i++) heapu8[langPtr + i] = language.codePointAt(i); exports.hb_buffer_set_language(this.ptr, exports.hb_language_from_string(langPtr, len)); } setScript(script) { exports.hb_buffer_set_script(this.ptr, script); } setClusterLevel(level) { exports.hb_buffer_set_cluster_level(this.ptr, level); } getGlyphInfos() { const length = exports.hb_buffer_get_length(this.ptr); const infosPtr = exports.hb_buffer_get_glyph_infos(this.ptr, 0); const infosPtr32 = infosPtr / 4; return heapu32.subarray(infosPtr32, infosPtr32 + 5 * length); } getGlyphPositions() { const length = exports.hb_buffer_get_length(this.ptr); const positionsPtr32 = exports.hb_buffer_get_glyph_positions(this.ptr, 0) / 4; return heapi32.subarray(positionsPtr32, positionsPtr32 + 5 * length); } getGlyphFlags(glyphIndex) { const infosPtr = exports.hb_buffer_get_glyph_infos(this.ptr, 0); return exports.hb_glyph_info_get_glyph_flags(infosPtr + glyphIndex * 20); } extractGlyphs() { const glyphsPtr = exports.hbjs_extract_glyphs(this.ptr); const glyphsPtr32 = glyphsPtr >>> 2; const ret = heapi32.slice(glyphsPtr32, glyphsPtr32 + this.getLength() * 7); exports.free(glyphsPtr); return ret; } destroy() { exports.hb_buffer_destroy(this.ptr); } } export function createBuffer() { return new HbBuffer(exports.hb_buffer_create()); } export function shape(font, buffer) { exports.hb_shape(font.ptr, buffer.ptr, 0, 0); } export function allocateUint16Array(size) { const ptr = exports.malloc(size * 2); const array = new Uint16Array(exports.memory.buffer, ptr, size); return { array, destroy: function () { exports.free(ptr); } }; }