/** * A factory wrapper parsing a font file using Typr. * Also adds support for WOFF files (not WOFF2). */ import typrFactory from '../libs/typr.factory.js' import woff2otfFactory from '../libs/woff2otf.factory.js' import { defineWorkerModule } from 'troika-worker-utils' /** * @typedef ParsedFont * @property {number} ascender * @property {number} descender * @property {number} xHeight * @property {(number) => boolean} supportsCodePoint * @property {(text:string, fontSize:number, letterSpacing:number, callback) => number} forEachGlyph * @property {number} lineGap * @property {number} capHeight * @property {number} unitsPerEm */ /** * @typedef {(buffer: ArrayBuffer) => ParsedFont} FontParser */ /** * @returns {FontParser} */ function parserFactory(Typr, woff2otf) { const cmdArgLengths = { M: 2, L: 2, Q: 4, C: 6, Z: 0 } // {joinType: "skip+step,..."} const joiningTypeRawData = {"C":"18g,ca,368,1kz","D":"17k,6,2,2+4,5+c,2+6,2+1,10+1,9+f,j+11,2+1,a,2,2+1,15+2,3,j+2,6+3,2+8,2,2,2+1,w+a,4+e,3+3,2,3+2,3+5,23+w,2f+4,3,2+9,2,b,2+3,3,1k+9,6+1,3+1,2+2,2+d,30g,p+y,1,1+1g,f+x,2,sd2+1d,jf3+4,f+3,2+4,2+2,b+3,42,2,4+2,2+1,2,3,t+1,9f+w,2,el+2,2+g,d+2,2l,2+1,5,3+1,2+1,2,3,6,16wm+1v","R":"17m+3,2,2,6+3,m,15+2,2+2,h+h,13,3+8,2,2,3+1,2,p+1,x,5+4,5,a,2,2,3,u,c+2,g+1,5,2+1,4+1,5j,6+1,2,b,2+2,f,2+1,1s+2,2,3+1,7,1ez0,2,2+1,4+4,b,4,3,b,42,2+2,4,3,2+1,2,o+3,ae,ep,x,2o+2,3+1,3,5+1,6","L":"x9u,jff,a,fd,jv","T":"4t,gj+33,7o+4,1+1,7c+18,2,2+1,2+1,2,21+a,2,1b+k,h,2u+6,3+5,3+1,2+3,y,2,v+q,2k+a,1n+8,a,p+3,2+8,2+2,2+4,18+2,3c+e,2+v,1k,2,5+7,5,4+6,b+1,u,1n,5+3,9,l+1,r,3+1,1m,5+1,5+1,3+2,4,v+1,4,c+1,1m,5+4,2+1,5,l+1,n+5,2,1n,3,2+3,9,8+1,c+1,v,1q,d,1f,4,1m+2,6+2,2+3,8+1,c+1,u,1n,3,7,6+1,l+1,t+1,1m+1,5+3,9,l+1,u,21,8+2,2,2j,3+6,d+7,2r,3+8,c+5,23+1,s,2,2,1k+d,2+4,2+1,6+a,2+z,a,2v+3,2+5,2+1,3+1,q+1,5+2,h+3,e,3+1,7,g,jk+2,qb+2,u+2,u+1,v+1,1t+1,2+6,9,3+a,a,1a+2,3c+1,z,3b+2,5+1,a,7+2,64+1,3,1n,2+6,2,2,3+7,7+9,3,1d+d,1,1+1,1s+3,1d,2+4,2,6,15+8,d+1,x+3,3+1,2+2,1l,2+1,4,2+2,1n+7,3+1,49+2,2+c,2+6,5,7,4+1,5j+1l,2+4,ek,3+1,r+4,1e+4,6+5,2p+c,1+3,1,1+2,1+b,2db+2,3y,2p+v,ff+3,30+1,n9x,1+2,2+9,x+1,29+1,7l,4,5,q+1,6,48+1,r+h,e,13+7,q+a,1b+2,1d,3+3,3+1,14,1w+5,3+1,3+1,d,9,1c,1g,2+2,3+1,6+1,2,17+1,9,6n,3,5,fn5,ki+f,h+f,5s,6y+2,ea,6b,46+4,1af+2,2+1,6+3,15+2,5,4m+1,fy+3,as+1,4a+a,4x,1j+e,1l+2,1e+3,3+1,1y+2,11+4,2+7,1r,d+1,1h+8,b+3,3,2o+2,3,2+1,7,4h,4+7,m+1,1m+1,4,12+6,4+4,5g+7,3+2,2,o,2d+5,2,5+1,2+1,6n+3,7+1,2+1,s+1,2e+7,3,2+1,2z,2,3+5,2,2u+2,3+3,2+4,78+8,2+1,75+1,2,5,41+3,3+1,5,x+9,15+5,3+3,9,a+5,3+2,1b+c,2+1,bb+6,2+5,2,2b+l,3+6,2+1,2+1,3f+5,4,2+1,2+6,2,21+1,4,2,9o+1,470+8,at4+4,1o+6,t5,1s+3,2a,f5l+1,2+3,43o+2,a+7,1+7,3+6,v+3,45+2,1j0+1i,5+1d,9,f,n+4,2+e,11t+6,2+g,3+6,2+1,2+4,7a+6,c6+3,15t+6,32+6,1,gzau,v+2n,3l+6n"} const JT_LEFT = 1, //indicates that a character joins with the subsequent character, but does not join with the preceding character. JT_RIGHT = 2, //indicates that a character joins with the preceding character, but does not join with the subsequent character. JT_DUAL = 4, //indicates that a character joins with the preceding character and joins with the subsequent character. JT_TRANSPARENT = 8, //indicates that the character does not join with adjacent characters and that the character must be skipped over when the shaping engine is evaluating the joining positions in a sequence of characters. When a JT_TRANSPARENT character is encountered in a sequence, the JOINING_TYPE of the preceding character passes through. Diacritical marks are frequently assigned this value. JT_JOIN_CAUSING = 16, //indicates that the character forces the use of joining forms with the preceding and subsequent characters. Kashidas and the Zero Width Joiner (U+200D) are both JOIN_CAUSING characters. JT_NON_JOINING = 32 //indicates that a character does not join with the preceding or with the subsequent character., let joiningTypeMap function getCharJoiningType(ch) { if (!joiningTypeMap) { const m = { R: JT_RIGHT, L: JT_LEFT, D: JT_DUAL, C: JT_JOIN_CAUSING, U: JT_NON_JOINING, T: JT_TRANSPARENT } joiningTypeMap = new Map() for (let type in joiningTypeRawData) { let lastCode = 0 joiningTypeRawData[type].split(',').forEach(range => { let [skip, step] = range.split('+') skip = parseInt(skip,36) step = step ? parseInt(step, 36) : 0 joiningTypeMap.set(lastCode += skip, m[type]) for (let i = step; i--;) { joiningTypeMap.set(++lastCode, m[type]) } }) } } return joiningTypeMap.get(ch) || JT_NON_JOINING } const ISOL = 1, INIT = 2, FINA = 3, MEDI = 4 const formsToFeatures = [null, 'isol', 'init', 'fina', 'medi'] function detectJoiningForms(str) { // This implements the algorithm described here: // https://github.com/n8willis/opentype-shaping-documents/blob/master/opentype-shaping-arabic-general.md const joiningForms = new Uint8Array(str.length) let prevJoiningType = JT_NON_JOINING let prevForm = ISOL let prevIndex = -1 for (let i = 0; i < str.length; i++) { const code = str.codePointAt(i) let joiningType = getCharJoiningType(code) | 0 let form = ISOL if (joiningType & JT_TRANSPARENT) { continue } if (prevJoiningType & (JT_LEFT | JT_DUAL | JT_JOIN_CAUSING)) { if (joiningType & (JT_RIGHT | JT_DUAL | JT_JOIN_CAUSING)) { form = FINA // isol->init, fina->medi if (prevForm === ISOL || prevForm === FINA) { joiningForms[prevIndex]++ } } else if (joiningType & (JT_LEFT | JT_NON_JOINING)) { // medi->fina, init->isol if (prevForm === INIT || prevForm === MEDI) { joiningForms[prevIndex]-- } } } else if (prevJoiningType & (JT_RIGHT | JT_NON_JOINING)) { // medi->fina, init->isol if (prevForm === INIT || prevForm === MEDI) { joiningForms[prevIndex]-- } } prevForm = joiningForms[i] = form prevJoiningType = joiningType prevIndex = i if (code > 0xffff) i++ } // console.log(str.split('').map(ch => ch.codePointAt(0).toString(16))) // console.log(str.split('').map(ch => getCharJoiningType(ch.codePointAt(0)))) // console.log(Array.from(joiningForms).map(f => formsToFeatures[f] || 'none')) return joiningForms } function stringToGlyphs (font, str) { const glyphIds = [] for (let i = 0; i < str.length; i++) { const cc = str.codePointAt(i) if (cc > 0xffff) i++ glyphIds.push(Typr.U.codeToGlyph(font, cc)) } const gsub = font['GSUB'] if (gsub) { const {lookupList, featureList} = gsub let joiningForms const supportedFeatures = /^(rlig|liga|mset|isol|init|fina|medi|half|pres|blws|ccmp)$/ const usedLookups = [] featureList.forEach(feature => { if (supportedFeatures.test(feature.tag)) { for (let ti = 0; ti < feature.tab.length; ti++) { if (usedLookups[feature.tab[ti]]) continue usedLookups[feature.tab[ti]] = true const tab = lookupList[feature.tab[ti]] const isJoiningFeature = /^(isol|init|fina|medi)$/.test(feature.tag) if (isJoiningFeature && !joiningForms) { //lazy joiningForms = detectJoiningForms(str) } for (let ci = 0; ci < glyphIds.length; ci++) { if (!joiningForms || !isJoiningFeature || formsToFeatures[joiningForms[ci]] === feature.tag) { Typr.U._applySubs(glyphIds, ci, tab, lookupList) } } } } }) } return glyphIds } // Calculate advances and x/y offsets for each glyph, e.g. kerning and mark // attachments. This is a more complete version of Typr.U.getPairAdjustment // and should become an upstream replacement eventually. function calcGlyphPositions(font, glyphIds) { const positions = new Int16Array(glyphIds.length * 3); // [offsetX, offsetY, advanceX, ...] let glyphIndex = 0; for (; glyphIndex < glyphIds.length; glyphIndex++) { const glyphId = glyphIds[glyphIndex] if (glyphId === -1) continue; positions[glyphIndex * 3 + 2] = font.hmtx.aWidth[glyphId]; // populate advanceX in...advance. const gpos = font.GPOS; if (gpos) { const llist = gpos.lookupList; for (let i = 0; i < llist.length; i++) { const lookup = llist[i]; for (let j = 0; j < lookup.tabs.length; j++) { const tab = lookup.tabs[j]; // Single char placement if (lookup.ltype === 1) { const ind = Typr._lctf.coverageIndex(tab.coverage, glyphId); if (ind !== -1 && tab.pos) { applyValueRecord(tab.pos, glyphIndex) break } } // Pairs (kerning) else if (lookup.ltype === 2) { let adj = null; let prevGlyphIndex = getPrevGlyphIndex() if (prevGlyphIndex !== -1) { const coverageIndex = Typr._lctf.coverageIndex(tab.coverage, glyphIds[prevGlyphIndex]); if (coverageIndex !== -1) { if (tab.fmt === 1) { const right = tab.pairsets[coverageIndex]; for (let k = 0; k < right.length; k++) { if (right[k].gid2 === glyphId) adj = right[k]; } } else if (tab.fmt === 2) { const c1 = Typr.U._getGlyphClass(glyphIds[prevGlyphIndex], tab.classDef1); const c2 = Typr.U._getGlyphClass(glyphId, tab.classDef2); adj = tab.matrix[c1][c2]; } if (adj) { if (adj.val1) applyValueRecord(adj.val1, prevGlyphIndex) if (adj.val2) applyValueRecord(adj.val2, glyphIndex) break } } } } // Mark to base else if (lookup.ltype === 4) { const markArrIndex = Typr._lctf.coverageIndex(tab.markCoverage, glyphId); if (markArrIndex !== -1) { const baseGlyphIndex = getPrevGlyphIndex(isBaseGlyph); const baseArrIndex = baseGlyphIndex === -1 ? -1 : Typr._lctf.coverageIndex(tab.baseCoverage, glyphIds[baseGlyphIndex]) if (baseArrIndex !== -1) { const markRecord = tab.markArray[markArrIndex]; const baseAnchor = tab.baseArray[baseArrIndex][markRecord.markClass]; positions[glyphIndex * 3] = baseAnchor.x - markRecord.x + positions[baseGlyphIndex * 3] - positions[baseGlyphIndex * 3 + 2] positions[glyphIndex * 3 + 1] = baseAnchor.y - markRecord.y + positions[baseGlyphIndex * 3 + 1]; break; } } } // Mark to mark else if (lookup.ltype === 6) { const mark1ArrIndex = Typr._lctf.coverageIndex(tab.mark1Coverage, glyphId); if (mark1ArrIndex !== -1) { const prevGlyphIndex = getPrevGlyphIndex(); if (prevGlyphIndex !== -1) { const prevGlyphId = glyphIds[prevGlyphIndex] if (getGlyphClass(font, prevGlyphId) === 3) { // only check mark glyphs const mark2ArrIndex = Typr._lctf.coverageIndex(tab.mark2Coverage, prevGlyphId) if (mark2ArrIndex !== -1) { const mark1Record = tab.mark1Array[mark1ArrIndex]; const mark2Anchor = tab.mark2Array[mark2ArrIndex][mark1Record.markClass]; positions[glyphIndex * 3] = mark2Anchor.x - mark1Record.x + positions[prevGlyphIndex * 3] - positions[prevGlyphIndex * 3 + 2]; positions[glyphIndex * 3 + 1] = mark2Anchor.y - mark1Record.y + positions[prevGlyphIndex * 3 + 1]; break; } } } } } } } } // Check kern table if no GPOS else if (font.kern && !font.cff) { const prevGlyphIndex = getPrevGlyphIndex(); if (prevGlyphIndex !== -1) { const ind1 = font.kern.glyph1.indexOf(glyphIds[prevGlyphIndex]); if (ind1 !== -1) { const ind2 = font.kern.rval[ind1].glyph2.indexOf(glyphId); if (ind2 !== -1) { positions[prevGlyphIndex * 3 + 2] += font.kern.rval[ind1].vals[ind2]; } } } } } return positions; function getPrevGlyphIndex(filter) { for (let i = glyphIndex - 1; i >=0; i--) { if (glyphIds[i] !== -1 && (!filter || filter(glyphIds[i]))) { return i } } return -1; } function isBaseGlyph(glyphId) { return getGlyphClass(font, glyphId) === 1; } function applyValueRecord(source, gi) { for (let i = 0; i < 3; i++) { positions[gi * 3 + i] += source[i] || 0 } } } function getGlyphClass(font, glyphId) { const classDef = font.GDEF && font.GDEF.glyphClassDef return classDef ? Typr.U._getGlyphClass(glyphId, classDef) : 0; } function firstNum(...args) { for (let i = 0; i < args.length; i++) { if (typeof args[i] === 'number') { return args[i] } } } /** * @returns ParsedFont */ function wrapFontObj(typrFont) { const glyphMap = Object.create(null) const os2 = typrFont['OS/2'] const hhea = typrFont.hhea const unitsPerEm = typrFont.head.unitsPerEm const ascender = firstNum(os2 && os2.sTypoAscender, hhea && hhea.ascender, unitsPerEm) /** @type ParsedFont */ const fontObj = { unitsPerEm, ascender, descender: firstNum(os2 && os2.sTypoDescender, hhea && hhea.descender, 0), capHeight: firstNum(os2 && os2.sCapHeight, ascender), xHeight: firstNum(os2 && os2.sxHeight, ascender), lineGap: firstNum(os2 && os2.sTypoLineGap, hhea && hhea.lineGap), supportsCodePoint(code) { return Typr.U.codeToGlyph(typrFont, code) > 0 }, forEachGlyph(text, fontSize, letterSpacing, callback) { let penX = 0 const fontScale = 1 / fontObj.unitsPerEm * fontSize const glyphIds = stringToGlyphs(typrFont, text) let charIndex = 0 const positions = calcGlyphPositions(typrFont, glyphIds) glyphIds.forEach((glyphId, i) => { // Typr returns a glyph index per string codepoint, with -1s in place of those that // were omitted due to ligature substitution. So we can track original index in the // string via simple increment, and skip everything else when seeing a -1. if (glyphId !== -1) { let glyphObj = glyphMap[glyphId] if (!glyphObj) { const {cmds, crds} = Typr.U.glyphToPath(typrFont, glyphId) // Build path string let path = '' let crdsIdx = 0 for (let i = 0, len = cmds.length; i < len; i++) { const numArgs = cmdArgLengths[cmds[i]] path += cmds[i] for (let j = 1; j <= numArgs; j++) { path += (j > 1 ? ',' : '') + crds[crdsIdx++] } } // Find extents - Glyf gives this in metadata but not CFF, and Typr doesn't // normalize the two, so it's simplest just to iterate ourselves. let xMin, yMin, xMax, yMax if (crds.length) { xMin = yMin = Infinity xMax = yMax = -Infinity for (let i = 0, len = crds.length; i < len; i += 2) { let x = crds[i] let y = crds[i + 1] if (x < xMin) xMin = x if (y < yMin) yMin = y if (x > xMax) xMax = x if (y > yMax) yMax = y } } else { xMin = xMax = yMin = yMax = 0 } glyphObj = glyphMap[glyphId] = { index: glyphId, advanceWidth: typrFont.hmtx.aWidth[glyphId], xMin, yMin, xMax, yMax, path, } } callback.call( null, glyphObj, penX + positions[i * 3] * fontScale, positions[i * 3 + 1] * fontScale, charIndex ) penX += positions[i * 3 + 2] * fontScale if (letterSpacing) { penX += letterSpacing * fontSize } } charIndex += (text.codePointAt(charIndex) > 0xffff ? 2 : 1) }) return penX } } return fontObj } /** * @type FontParser */ return function parse(buffer) { // Look to see if we have a WOFF file and convert it if so: const peek = new Uint8Array(buffer, 0, 4) const tag = Typr._bin.readASCII(peek, 0, 4) if (tag === 'wOFF') { buffer = woff2otf(buffer) } else if (tag === 'wOF2') { throw new Error('woff2 fonts not supported') } return wrapFontObj(Typr.parse(buffer)[0]) } } const workerModule = /*#__PURE__*/defineWorkerModule({ name: 'Typr Font Parser', dependencies: [typrFactory, woff2otfFactory, parserFactory], init(typrFactory, woff2otfFactory, parserFactory) { const Typr = typrFactory() const woff2otf = woff2otfFactory() return parserFactory(Typr, woff2otf) } }) export default workerModule