download
raw
8.88 kB
import fontParser from "./FontParser.js";
import unicodeFontResolverClientFactory from "../libs/unicode-font-resolver-client.factory.js";
import { defineWorkerModule } from "troika-worker-utils";
/**
* @typedef {string | {src:string, label?:string, unicodeRange?:string, lang?:string}} UserFont
*/
/**
* @typedef {ClientOptions} FontResolverOptions
* @property {Array<UserFont>|UserFont} [fonts]
* @property {'normal'|'italic'} [style]
* @property {'normal'|'bold'|number} [style]
* @property {string} [unicodeFontsURL]
*/
/**
* @typedef {Object} FontResolverResult
* @property {Uint8Array} chars
* @property {Array<ParsedFont & {src:string}>} fonts
*/
/**
* @typedef {function} FontResolver
* @param {string} text
* @param {(FontResolverResult) => void} callback
* @param {FontResolverOptions} [options]
*/
/**
* Factory for the FontResolver function.
* @param {FontParser} fontParser
* @param {{getFontsForString: function, CodePointSet: function}} unicodeFontResolverClient
* @return {FontResolver}
*/
export function createFontResolver(fontParser, unicodeFontResolverClient) {
/**
* @type {Record<string, ParsedFont>}
*/
const parsedFonts = Object.create(null)
/**
* @type {Record<string, Array<(ParsedFont) => void>>}
*/
const loadingFonts = Object.create(null)
/**
* Load a given font url
*/
function doLoadFont(url, callback) {
const onError = err => {
console.error(`Failure loading font ${url}`, err)
}
try {
const request = new XMLHttpRequest()
request.open('get', url, true)
request.responseType = 'arraybuffer'
request.onload = function () {
if (request.status >= 400) {
onError(new Error(request.statusText))
}
else if (request.status > 0) {
try {
const fontObj = fontParser(request.response)
fontObj.src = url;
callback(fontObj)
} catch (e) {
onError(e)
}
}
}
request.onerror = onError
request.send()
} catch(err) {
onError(err)
}
}
/**
* Load a given font url if needed, invoking a callback when it's loaded. If already
* loaded, the callback will be called synchronously.
* @param {string} fontUrl
* @param {(font: ParsedFont) => void} callback
*/
function loadFont(fontUrl, callback) {
let font = parsedFonts[fontUrl]
if (font) {
callback(font)
} else if (loadingFonts[fontUrl]) {
loadingFonts[fontUrl].push(callback)
} else {
loadingFonts[fontUrl] = [callback]
doLoadFont(fontUrl, fontObj => {
fontObj.src = fontUrl
parsedFonts[fontUrl] = fontObj
loadingFonts[fontUrl].forEach(cb => cb(fontObj))
delete loadingFonts[fontUrl];
})
}
}
/**
* For a given string of text, determine which fonts are required to fully render it and
* ensure those fonts are loaded.
*/
return function (text, callback, {
lang,
fonts: userFonts = [],
style = 'normal',
weight = 'normal',
unicodeFontsURL
} = {}) {
const charResolutions = new Uint8Array(text.length);
const fontResolutions = [];
if (!text.length) {
allDone()
}
const fontIndices = new Map();
const fallbackRanges = [] // [[start, end], ...]
if (style !== 'italic') style = 'normal'
if (typeof weight !== 'number') {
weight = weight === 'bold' ? 700 : 400
}
if (userFonts && !Array.isArray(userFonts)) {
userFonts = [userFonts]
}
userFonts = userFonts.slice()
// filter by language
.filter(def => !def.lang || def.lang.test(lang))
// switch order for easier iteration
.reverse()
if (userFonts.length) {
const UNKNOWN = 0
const RESOLVED = 1
const NEEDS_FALLBACK = 2
let prevCharResult = UNKNOWN
;(function resolveUserFonts (startIndex = 0) {
for (let i = startIndex, iLen = text.length; i < iLen; i++) {
const codePoint = text.codePointAt(i)
// Carry previous character's result forward if:
// - it resolved to a font that also covers this character
// - this character is whitespace
if (
(prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||
(i > 0 && /\s/.test(text[i]))
) {
charResolutions[i] = charResolutions[i - 1]
if (prevCharResult === NEEDS_FALLBACK) {
fallbackRanges[fallbackRanges.length - 1][1] = i
}
} else {
for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {
if (j === jLen) {
// none of the user fonts matched; needs fallback
const range = prevCharResult === NEEDS_FALLBACK ?
fallbackRanges[fallbackRanges.length - 1] :
(fallbackRanges[fallbackRanges.length] = [i, i])
range[1] = i;
prevCharResult = NEEDS_FALLBACK;
} else {
charResolutions[i] = j;
const { src, unicodeRange } = userFonts[j];
// filter by optional explicit unicode ranges
if (!unicodeRange || isCodeInRanges(codePoint, unicodeRange)) {
const fontObj = parsedFonts[src];
// font not yet loaded, load it and resume
if (!fontObj) {
loadFont(src, () => {
resolveUserFonts(i);
});
return;
}
// if the font actually contains a glyph for this char, lock it in
if (fontObj.supportsCodePoint(codePoint)) {
let fontIndex = fontIndices.get(fontObj);
if (typeof fontIndex !== 'number') {
fontIndex = fontResolutions.length;
fontResolutions.push(fontObj);
fontIndices.set(fontObj, fontIndex);
}
charResolutions[i] = fontIndex;
prevCharResult = RESOLVED;
break;
}
}
}
}
}
if (codePoint > 0xffff && i + 1 < iLen) {
charResolutions[i + 1] = charResolutions[i]
i++
if (prevCharResult === NEEDS_FALLBACK) {
fallbackRanges[fallbackRanges.length - 1][1] = i
}
}
}
resolveFallbacks();
})();
} else {
fallbackRanges.push([0, text.length - 1])
resolveFallbacks();
}
function resolveFallbacks() {
if (fallbackRanges.length) {
// Combine all fallback substrings into a single string for querying
const fallbackString = fallbackRanges.map(range => text.substring(range[0], range[1] + 1)).join('\n')
unicodeFontResolverClient.getFontsForString(fallbackString, {
lang: lang || undefined,
style,
weight,
dataUrl: unicodeFontsURL
}).then(({fontUrls, chars}) => {
// Extract results and put them back in the main array
const fontIndexOffset = fontResolutions.length
let charIdx = 0;
fallbackRanges.forEach(range => {
for (let i = 0, endIdx = range[1] - range[0]; i <= endIdx; i++) {
charResolutions[range[0] + i] = chars[charIdx++] + fontIndexOffset
}
charIdx++ //skip segment separator
})
// Load and parse the fallback fonts - avoiding Promise here to prevent polyfills in the worker
let loadedCount = 0;
fontUrls.forEach((url, i) => {
loadFont(url, fontObj => {
fontResolutions[i + fontIndexOffset] = fontObj
if (++loadedCount === fontUrls.length) {
allDone();
}
})
})
});
} else {
allDone();
}
}
function allDone() {
callback({
chars: charResolutions,
fonts: fontResolutions
})
}
function isCodeInRanges(code, ranges) {
// todo optimize search - CodePointSet from unicode-font-resolver?
for (let k = 0; k < ranges.length; k++) {
const [start, end = start] = ranges[k]
if (start <= code && code <= end) {
return true
}
}
return false
}
}
}
export const fontResolverWorkerModule = /*#__PURE__*/defineWorkerModule({
name: 'FontResolver',
dependencies: [
createFontResolver,
fontParser,
unicodeFontResolverClientFactory,
],
init(createFontResolver, fontParser, unicodeFontResolverClientFactory) {
return createFontResolver(fontParser, unicodeFontResolverClientFactory());
}
})

Xet Storage Details

Size:
8.88 kB
·
Xet hash:
cdbc0f85fa69729e4a459e8d5f5d7067119bfc45cdad8cd639d5406f86a11928

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.