19 changed files with 4741 additions and 91 deletions
@ -0,0 +1,202 @@ |
|||||||
|
/** |
||||||
|
* Modified version of hb-subset bindings from "subset-font" package https://github.com/papandreou/subset-font/blob/3f711c8aa29a426c7f22655861abfb976950f527/index.js
|
||||||
|
*
|
||||||
|
* CHANGELOG: |
||||||
|
* - removed dependency on node APIs to work inside the browser |
||||||
|
* - removed dependency on font fontverter for brotli compression |
||||||
|
* - removed dependencies on lodash and p-limit |
||||||
|
* - removed options for preserveNameIds, variationAxes, noLayoutClosure (not needed for now) |
||||||
|
* - replaced text input with codepoints |
||||||
|
* - rewritten in typescript and with esm modules
|
||||||
|
|
||||||
|
Copyright (c) 2012, Andreas Lind Petersen |
||||||
|
All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are |
||||||
|
met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer in |
||||||
|
the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of the author nor the names of contributors may |
||||||
|
be used to endorse or promote products derived from this |
||||||
|
software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
||||||
|
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||||
|
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A |
||||||
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
|
||||||
|
// function HB_TAG(str) {
|
||||||
|
// return str.split("").reduce((a, ch) => {
|
||||||
|
// return (a << 8) + ch.charCodeAt(0);
|
||||||
|
// }, 0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
function subset( |
||||||
|
hbSubsetWasm: any, |
||||||
|
heapu8: Uint8Array, |
||||||
|
font: ArrayBuffer, |
||||||
|
codePoints: ReadonlySet<number>, |
||||||
|
) { |
||||||
|
const input = hbSubsetWasm.hb_subset_input_create_or_fail(); |
||||||
|
if (input === 0) { |
||||||
|
throw new Error( |
||||||
|
"hb_subset_input_create_or_fail (harfbuzz) returned zero, indicating failure", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const fontBuffer = hbSubsetWasm.malloc(font.byteLength); |
||||||
|
heapu8.set(new Uint8Array(font), fontBuffer); |
||||||
|
|
||||||
|
// Create the face
|
||||||
|
const blob = hbSubsetWasm.hb_blob_create( |
||||||
|
fontBuffer, |
||||||
|
font.byteLength, |
||||||
|
2, // HB_MEMORY_MODE_WRITABLE
|
||||||
|
0, |
||||||
|
0, |
||||||
|
); |
||||||
|
const face = hbSubsetWasm.hb_face_create(blob, 0); |
||||||
|
hbSubsetWasm.hb_blob_destroy(blob); |
||||||
|
|
||||||
|
// Do the equivalent of --font-features=*
|
||||||
|
const layoutFeatures = hbSubsetWasm.hb_subset_input_set( |
||||||
|
input, |
||||||
|
6, // HB_SUBSET_SETS_LAYOUT_FEATURE_TAG
|
||||||
|
); |
||||||
|
hbSubsetWasm.hb_set_clear(layoutFeatures); |
||||||
|
hbSubsetWasm.hb_set_invert(layoutFeatures); |
||||||
|
|
||||||
|
// if (preserveNameIds) {
|
||||||
|
// const inputNameIds = harfbuzzJsWasm.hb_subset_input_set(
|
||||||
|
// input,
|
||||||
|
// 4, // HB_SUBSET_SETS_NAME_ID
|
||||||
|
// );
|
||||||
|
// for (const nameId of preserveNameIds) {
|
||||||
|
// harfbuzzJsWasm.hb_set_add(inputNameIds, nameId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (noLayoutClosure) {
|
||||||
|
// harfbuzzJsWasm.hb_subset_input_set_flags(
|
||||||
|
// input,
|
||||||
|
// harfbuzzJsWasm.hb_subset_input_get_flags(input) | 0x00000200, // HB_SUBSET_FLAGS_NO_LAYOUT_CLOSURE
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Add unicodes indices
|
||||||
|
const inputUnicodes = hbSubsetWasm.hb_subset_input_unicode_set(input); |
||||||
|
for (const c of codePoints) { |
||||||
|
hbSubsetWasm.hb_set_add(inputUnicodes, c); |
||||||
|
} |
||||||
|
|
||||||
|
// if (variationAxes) {
|
||||||
|
// for (const [axisName, value] of Object.entries(variationAxes)) {
|
||||||
|
// if (typeof value === "number") {
|
||||||
|
// // Simple case: Pin/instance the variation axis to a single value
|
||||||
|
// if (
|
||||||
|
// !harfbuzzJsWasm.hb_subset_input_pin_axis_location(
|
||||||
|
// input,
|
||||||
|
// face,
|
||||||
|
// HB_TAG(axisName),
|
||||||
|
// value,
|
||||||
|
// )
|
||||||
|
// ) {
|
||||||
|
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||||
|
// harfbuzzJsWasm.free(fontBuffer);
|
||||||
|
// throw new Error(
|
||||||
|
// `hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning ${axisName} to ${value}, indicating failure. Maybe the axis does not exist in the font?`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } else if (value && typeof value === "object") {
|
||||||
|
// // Complex case: Reduce the variation space of the axis
|
||||||
|
// if (
|
||||||
|
// typeof value.min === "undefined" ||
|
||||||
|
// typeof value.max === "undefined"
|
||||||
|
// ) {
|
||||||
|
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||||
|
// harfbuzzJsWasm.free(fontBuffer);
|
||||||
|
// throw new Error(
|
||||||
|
// `${axisName}: You must provide both a min and a max value when setting the axis range`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// if (
|
||||||
|
// !harfbuzzJsWasm.hb_subset_input_set_axis_range(
|
||||||
|
// input,
|
||||||
|
// face,
|
||||||
|
// HB_TAG(axisName),
|
||||||
|
// value.min,
|
||||||
|
// value.max,
|
||||||
|
// // An explicit NaN makes harfbuzz use the existing default value, clamping to the new range if necessary
|
||||||
|
// value.default ?? NaN,
|
||||||
|
// )
|
||||||
|
// ) {
|
||||||
|
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||||
|
// harfbuzzJsWasm.free(fontBuffer);
|
||||||
|
// throw new Error(
|
||||||
|
// `hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of ${axisName} to [${value.min}; ${value.max}] and a default value of ${value.default}, indicating failure. Maybe the axis does not exist in the font?`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
let subset; |
||||||
|
try { |
||||||
|
subset = hbSubsetWasm.hb_subset_or_fail(face, input); |
||||||
|
if (subset === 0) { |
||||||
|
hbSubsetWasm.hb_face_destroy(face); |
||||||
|
hbSubsetWasm.free(fontBuffer); |
||||||
|
throw new Error( |
||||||
|
"hb_subset_or_fail (harfbuzz) returned zero, indicating failure. Maybe the input file is corrupted?", |
||||||
|
); |
||||||
|
} |
||||||
|
} finally { |
||||||
|
// Clean up
|
||||||
|
hbSubsetWasm.hb_subset_input_destroy(input); |
||||||
|
} |
||||||
|
|
||||||
|
// Get result blob
|
||||||
|
const result = hbSubsetWasm.hb_face_reference_blob(subset); |
||||||
|
|
||||||
|
const offset = hbSubsetWasm.hb_blob_get_data(result, 0); |
||||||
|
const subsetByteLength = hbSubsetWasm.hb_blob_get_length(result); |
||||||
|
if (subsetByteLength === 0) { |
||||||
|
hbSubsetWasm.hb_blob_destroy(result); |
||||||
|
hbSubsetWasm.hb_face_destroy(subset); |
||||||
|
hbSubsetWasm.hb_face_destroy(face); |
||||||
|
hbSubsetWasm.free(fontBuffer); |
||||||
|
throw new Error( |
||||||
|
"Failed to create subset font, maybe the input file is corrupted?", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const subsetFont = new Uint8Array( |
||||||
|
heapu8.subarray(offset, offset + subsetByteLength), |
||||||
|
); |
||||||
|
|
||||||
|
// Clean up
|
||||||
|
hbSubsetWasm.hb_blob_destroy(result); |
||||||
|
hbSubsetWasm.hb_face_destroy(subset); |
||||||
|
hbSubsetWasm.hb_face_destroy(face); |
||||||
|
hbSubsetWasm.free(fontBuffer); |
||||||
|
|
||||||
|
return subsetFont; |
||||||
|
} |
||||||
|
|
||||||
|
export default { |
||||||
|
subset, |
||||||
|
}; |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
/** |
||||||
|
* Lazy loads wasm and respective bindings for font subsetting based on the harfbuzzjs. |
||||||
|
*/ |
||||||
|
let loadedWasm: ReturnType<typeof load> | null = null; |
||||||
|
|
||||||
|
// TODO: add support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||||
|
const load = (): Promise<{ |
||||||
|
subset: ( |
||||||
|
fontBuffer: ArrayBuffer, |
||||||
|
codePoints: ReadonlySet<number>, |
||||||
|
) => Uint8Array; |
||||||
|
}> => { |
||||||
|
return new Promise(async (resolve, reject) => { |
||||||
|
try { |
||||||
|
const [binary, bindings] = await Promise.all([ |
||||||
|
import("./hb-subset.wasm"), |
||||||
|
import("./hb-subset.bindings"), |
||||||
|
]); |
||||||
|
|
||||||
|
WebAssembly.instantiate(binary.default).then((module) => { |
||||||
|
try { |
||||||
|
const harfbuzzJsWasm = module.instance.exports; |
||||||
|
// @ts-expect-error since `.buffer` is custom prop
|
||||||
|
const heapu8 = new Uint8Array(harfbuzzJsWasm.memory.buffer); |
||||||
|
|
||||||
|
const hbSubset = { |
||||||
|
subset: ( |
||||||
|
fontBuffer: ArrayBuffer, |
||||||
|
codePoints: ReadonlySet<number>, |
||||||
|
) => { |
||||||
|
return bindings.default.subset( |
||||||
|
harfbuzzJsWasm, |
||||||
|
heapu8, |
||||||
|
fontBuffer, |
||||||
|
codePoints, |
||||||
|
); |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
resolve(hbSubset); |
||||||
|
} catch (e) { |
||||||
|
reject(e); |
||||||
|
} |
||||||
|
}); |
||||||
|
} catch (error) { |
||||||
|
reject(error); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// lazy load the default export
|
||||||
|
export default (): ReturnType<typeof load> => { |
||||||
|
if (!loadedWasm) { |
||||||
|
loadedWasm = load(); |
||||||
|
} |
||||||
|
|
||||||
|
return loadedWasm; |
||||||
|
}; |
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,70 @@ |
|||||||
|
/** |
||||||
|
* Lazy loads wasm and respective bindings for woff2 compression and decompression. |
||||||
|
*/ |
||||||
|
type Vector = any; |
||||||
|
|
||||||
|
let loadedWasm: ReturnType<typeof load> | null = null; |
||||||
|
|
||||||
|
// TODO: add support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||||
|
const load = (): Promise<{ |
||||||
|
compress: (buffer: ArrayBuffer) => Uint8Array; |
||||||
|
decompress: (buffer: ArrayBuffer) => Uint8Array; |
||||||
|
}> => { |
||||||
|
return new Promise(async (resolve, reject) => { |
||||||
|
try { |
||||||
|
const [binary, bindings] = await Promise.all([ |
||||||
|
import("./woff2.wasm"), |
||||||
|
import("./woff2.bindings"), |
||||||
|
]); |
||||||
|
|
||||||
|
// initializing the module manually, so that we could pass in the wasm binary
|
||||||
|
bindings |
||||||
|
.default({ wasmBinary: binary.default }) |
||||||
|
.then( |
||||||
|
(module: { |
||||||
|
woff2Enc: (buffer: ArrayBuffer, byteLength: number) => Vector; |
||||||
|
woff2Dec: (buffer: ArrayBuffer, byteLength: number) => Vector; |
||||||
|
}) => { |
||||||
|
try { |
||||||
|
// re-map from internal vector into byte array
|
||||||
|
function convertFromVecToUint8Array(vector: Vector): Uint8Array { |
||||||
|
const arr = []; |
||||||
|
for (let i = 0, l = vector.size(); i < l; i++) { |
||||||
|
arr.push(vector.get(i)); |
||||||
|
} |
||||||
|
|
||||||
|
return new Uint8Array(arr); |
||||||
|
} |
||||||
|
|
||||||
|
// re-exporting only compress and decompress functions (also avoids infinite loop inside emscripten bindings)
|
||||||
|
const woff2 = { |
||||||
|
compress: (buffer: ArrayBuffer) => |
||||||
|
convertFromVecToUint8Array( |
||||||
|
module.woff2Enc(buffer, buffer.byteLength), |
||||||
|
), |
||||||
|
decompress: (buffer: ArrayBuffer) => |
||||||
|
convertFromVecToUint8Array( |
||||||
|
module.woff2Dec(buffer, buffer.byteLength), |
||||||
|
), |
||||||
|
}; |
||||||
|
|
||||||
|
resolve(woff2); |
||||||
|
} catch (e) { |
||||||
|
reject(e); |
||||||
|
} |
||||||
|
}, |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
reject(e); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// lazy loaded default export
|
||||||
|
export default (): ReturnType<typeof load> => { |
||||||
|
if (!loadedWasm) { |
||||||
|
loadedWasm = load(); |
||||||
|
} |
||||||
|
|
||||||
|
return loadedWasm; |
||||||
|
}; |
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,75 @@ |
|||||||
|
/** |
||||||
|
* This script is used to convert the wasm modules into js modules, with the binary converted into base64 encoded strings. |
||||||
|
*/ |
||||||
|
const fs = require("fs"); |
||||||
|
const path = require("path"); |
||||||
|
|
||||||
|
const wasmModules = [ |
||||||
|
{ |
||||||
|
pkg: `../node_modules/fonteditor-core`, |
||||||
|
src: `./wasm/woff2.wasm`, |
||||||
|
dest: `../packages/excalidraw/fonts/wasm/woff2.wasm.ts`, |
||||||
|
}, |
||||||
|
{ |
||||||
|
pkg: `../node_modules/harfbuzzjs`, |
||||||
|
src: `./wasm/hb-subset.wasm`, |
||||||
|
dest: `../packages/excalidraw/fonts/wasm/hb-subset.wasm.ts`, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
for (const { pkg, src, dest } of wasmModules) { |
||||||
|
const packagePath = path.resolve(__dirname, pkg, "package.json"); |
||||||
|
const licensePath = path.resolve(__dirname, pkg, "LICENSE"); |
||||||
|
const sourcePath = path.resolve(__dirname, src); |
||||||
|
const destPath = path.resolve(__dirname, dest); |
||||||
|
|
||||||
|
const { |
||||||
|
name, |
||||||
|
version, |
||||||
|
author, |
||||||
|
license, |
||||||
|
authors, |
||||||
|
licenses, |
||||||
|
} = require(packagePath); |
||||||
|
|
||||||
|
const licenseContent = fs.readFileSync(licensePath, "utf-8") || ""; |
||||||
|
const base64 = fs.readFileSync(sourcePath, "base64"); |
||||||
|
const content = `// GENERATED CODE -- DO NOT EDIT!
|
||||||
|
/* eslint-disable prettier/prettier */ |
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
/** |
||||||
|
* The following wasm module is generated with \`scripts/buildWasm.js\` and encoded as base64.
|
||||||
|
* |
||||||
|
* The source of this content is taken from the package "${name}", which contains the following metadata: |
||||||
|
*
|
||||||
|
* @author ${author || JSON.stringify(authors)}
|
||||||
|
* @license ${license || JSON.stringify(licenses)} |
||||||
|
* @version ${version} |
||||||
|
|
||||||
|
${licenseContent} |
||||||
|
*/ |
||||||
|
|
||||||
|
// faster atob alternative - https://github.com/evanw/esbuild/issues/1534#issuecomment-902738399
|
||||||
|
const __toBinary = /* @__PURE__ */ (() => { |
||||||
|
const table = new Uint8Array(128); |
||||||
|
for (let i = 0; i < 64; i++) |
||||||
|
{table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;} |
||||||
|
return (base64) => { |
||||||
|
const n = base64.length; const bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0); |
||||||
|
for (let i2 = 0, j = 0; i2 < n; ) { |
||||||
|
const c0 = table[base64.charCodeAt(i2++)]; const c1 = table[base64.charCodeAt(i2++)]; |
||||||
|
const c2 = table[base64.charCodeAt(i2++)]; const c3 = table[base64.charCodeAt(i2++)]; |
||||||
|
bytes[j++] = c0 << 2 | c1 >> 4; |
||||||
|
bytes[j++] = c1 << 4 | c2 >> 2; |
||||||
|
bytes[j++] = c2 << 6 | c3; |
||||||
|
} |
||||||
|
return bytes; |
||||||
|
}; |
||||||
|
})(); |
||||||
|
|
||||||
|
export default __toBinary(\`${base64}\`);
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(destPath, content); |
||||||
|
} |
||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue