Buckets:
| /** | |
| * 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 | |
Xet Storage Details
- Size:
- 17.8 kB
- Xet hash:
- 9ac175ae7c64cbbf5841220507832dfff177bad151452ae53fc94a8cb922f1db
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.