├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples ├── get-font-name.js ├── introduction.js └── lib-font-in-the-browser.html ├── fonts ├── AthenaRuby_b018.ttf ├── IBMPlexSansThai-Light.ttf ├── MehrNastaliqWeb-Regular.ttf ├── OpenSans │ ├── Apache License.txt │ ├── OpenSans-Bold.ttf │ ├── OpenSans-BoldItalic.ttf │ ├── OpenSans-ExtraBold.ttf │ ├── OpenSans-ExtraBoldItalic.ttf │ ├── OpenSans-Italic.ttf │ ├── OpenSans-Light.ttf │ ├── OpenSans-LightItalic.ttf │ ├── OpenSans-Regular.ttf │ ├── OpenSans-Semibold.ttf │ └── OpenSans-SemiboldItalic.ttf ├── README.md ├── Recursive_VF_1.064.ttf ├── SourceCodePro │ ├── SourceCodePro-Regular.otf │ ├── SourceCodePro-Regular.otf.woff │ ├── SourceCodePro-Regular.otf.woff2 │ ├── SourceCodePro-Regular.ttf │ ├── SourceCodePro-Regular.ttf.woff │ ├── SourceCodePro-Regular.ttf.woff2 │ ├── SourceCodeVariable-Roman.otf │ ├── SourceCodeVariable-Roman.otf.woff │ ├── SourceCodeVariable-Roman.otf.woff2 │ ├── SourceCodeVariable-Roman.ttf │ ├── SourceCodeVariable-Roman.ttf.woff │ └── SourceCodeVariable-Roman.ttf.woff2 ├── issue-114 │ ├── Flaticon.woff2 │ ├── README.md │ ├── roboto-regular.woff2 │ └── tawk-font-icon-2.woff2 ├── issue-123 │ └── Castoro-Regular.woff2 ├── issue-127 │ └── Abelone-FREE.otf └── issue-130 │ ├── ABeeZee-Regular.ttf │ ├── AbyssinicaSIL-Regular.ttf │ ├── AkayaKanadaka-Regular.ttf │ └── Alice-Regular.ttf ├── index.html ├── lib-font.browser.js ├── lib-font.js ├── lib ├── inflate.js └── unbrotli.js ├── package.json ├── src ├── eventing.js ├── lazy.js ├── opentype │ ├── index.js │ ├── sfnt.js │ ├── tables │ │ ├── advanced │ │ │ ├── BASE.js │ │ │ ├── GDEF.js │ │ │ ├── GPOS.js │ │ │ ├── GSUB.js │ │ │ ├── JSTF.js │ │ │ ├── MATH.js │ │ │ ├── README.md │ │ │ ├── lookups │ │ │ │ ├── gpos │ │ │ │ │ ├── gpos-lookup.js │ │ │ │ │ ├── lookup-type-1.js │ │ │ │ │ ├── lookup-type-2.js │ │ │ │ │ ├── lookup-type-3.js │ │ │ │ │ ├── lookup-type-4.js │ │ │ │ │ ├── lookup-type-5.js │ │ │ │ │ ├── lookup-type-6.js │ │ │ │ │ ├── lookup-type-7.js │ │ │ │ │ ├── lookup-type-8.js │ │ │ │ │ └── lookup-type-9.js │ │ │ │ └── gsub │ │ │ │ │ ├── gsub-lookup.js │ │ │ │ │ ├── lookup-type-1.js │ │ │ │ │ ├── lookup-type-2.js │ │ │ │ │ ├── lookup-type-3.js │ │ │ │ │ ├── lookup-type-4.js │ │ │ │ │ ├── lookup-type-5.js │ │ │ │ │ ├── lookup-type-6.js │ │ │ │ │ ├── lookup-type-7.js │ │ │ │ │ └── lookup-type-8.js │ │ │ └── shared │ │ │ │ ├── class.js │ │ │ │ ├── coverage.js │ │ │ │ ├── feature.js │ │ │ │ ├── itemvariation.js │ │ │ │ ├── lookup.js │ │ │ │ ├── script.js │ │ │ │ └── subtables │ │ │ │ ├── gpos.js │ │ │ │ └── gsub.js │ │ ├── common-layout-table.js │ │ ├── createTable.js │ │ ├── simple-table.js │ │ └── simple │ │ │ ├── OS2.js │ │ │ ├── SVG.js │ │ │ ├── bitmap │ │ │ ├── CBDT.js │ │ │ ├── CBLC.js │ │ │ ├── EBDT.js │ │ │ ├── EBLC.js │ │ │ ├── EBSC.js │ │ │ ├── formats │ │ │ │ ├── format1.js │ │ │ │ ├── format17.js │ │ │ │ ├── format18.js │ │ │ │ ├── format19.js │ │ │ │ ├── format2.js │ │ │ │ ├── format3.js │ │ │ │ ├── format4.js │ │ │ │ ├── format5.js │ │ │ │ ├── format6.js │ │ │ │ ├── format7.js │ │ │ │ ├── format8.js │ │ │ │ └── format9.js │ │ │ ├── sbix.js │ │ │ ├── shared.js │ │ │ └── subtables │ │ │ │ ├── subtable1.js │ │ │ │ ├── subtable2.js │ │ │ │ ├── subtable3.js │ │ │ │ ├── subtable4.js │ │ │ │ └── subtable5.js │ │ │ ├── cff │ │ │ ├── CFF.js │ │ │ ├── CFF2.js │ │ │ └── VORG.js │ │ │ ├── cmap.js │ │ │ ├── cmap │ │ │ ├── README.md │ │ │ ├── createSubTable.js │ │ │ ├── format0.js │ │ │ ├── format10.js │ │ │ ├── format12.js │ │ │ ├── format13.js │ │ │ ├── format14.js │ │ │ ├── format2.js │ │ │ ├── format4.js │ │ │ ├── format6.js │ │ │ ├── format8.js │ │ │ └── subtable.js │ │ │ ├── color │ │ │ ├── COLR.js │ │ │ └── CPAL.js │ │ │ ├── head.js │ │ │ ├── hhea.js │ │ │ ├── hmtx.js │ │ │ ├── maxp.js │ │ │ ├── name.js │ │ │ ├── other │ │ │ ├── DSIG.js │ │ │ ├── LTSH.js │ │ │ ├── MERG.js │ │ │ ├── PCLT.js │ │ │ ├── VDMX.js │ │ │ ├── hdmx.js │ │ │ ├── kern.js │ │ │ ├── meta.js │ │ │ ├── vhea.js │ │ │ └── vmtx.js │ │ │ ├── post.js │ │ │ ├── ttf │ │ │ ├── cvt.js │ │ │ ├── fpgm.js │ │ │ ├── gasp.js │ │ │ ├── glyf.js │ │ │ ├── loca.js │ │ │ └── prep.js │ │ │ └── variation │ │ │ ├── HVAR.js │ │ │ ├── MVAR.js │ │ │ ├── STAT.js │ │ │ ├── VVAR.js │ │ │ ├── avar.js │ │ │ ├── cvar.js │ │ │ ├── fvar.js │ │ │ └── gvar.js │ ├── woff.js │ └── woff2.js ├── parser.js └── utils │ ├── fontface.js │ ├── shim-fetch.js │ └── validator.js └── testing ├── browser ├── matrix │ ├── index.html │ └── index.js ├── puppeteer.js ├── server.js ├── test.js └── tests │ ├── assert.js │ ├── test-SFNT.js │ ├── test.otf.js │ └── test.ttf.js ├── gsub-recipe.js ├── manual ├── custom │ ├── brush-grotesk-woff2-gsub.js │ ├── castoro-parsing.js │ ├── flaticon-parsing.js │ ├── issue-127.js │ ├── issue-130.js │ ├── issue-85.js │ └── test.out ├── index.html └── index.js └── node ├── athena.ruby.test.js ├── font-profiles ├── athena-ruby.js ├── mehr-nastaliq.js ├── profiles.js └── source-code-pro.js ├── gsub └── test-gsub.js ├── issue-114 └── flaticon.test.js ├── mehr.nastaliq.test.js └── source.code.pro.test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: Pomax 4 | patreon: Bezierinfo 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | protected 3 | node_modules 4 | package-lock.json 5 | test.js 6 | fonts/proprietary 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | fonts 4 | node_modules 5 | protected 6 | testing 7 | .gitignore 8 | index.html 9 | /index.js 10 | test.js 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "DLFT", 4 | "calt", 5 | "ccmp", 6 | "cyrl", 7 | "dflt", 8 | "dlig", 9 | "dnom", 10 | "fina", 11 | "grek", 12 | "isol", 13 | "langsys", 14 | "latn", 15 | "locl", 16 | "medi", 17 | "mset", 18 | "n", 19 | "numr", 20 | "onum", 21 | "rlig", 22 | "sinf" 23 | ] 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 pomax@nihongoresources.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /examples/get-font-name.js: -------------------------------------------------------------------------------- 1 | // This is a Node.js script that loads a font and prints its name. 2 | // 3 | // Run it like this from the command line: 4 | // 5 | // $ node get-font-name.js 6 | // 7 | // It will use the "Recursive" test font in LibFont's ./fonts 8 | // directory, but you can point it to a different font by 9 | // providing the path to the font: 10 | // 11 | // $ node get-font-name.js path/to/font.ttf 12 | 13 | // Import the LibFont library 14 | import { Font } from "../lib-font.js"; 15 | 16 | // Create a LibFont object and give it a name 17 | const font = new Font("My Font Name"); 18 | 19 | // Set the source font file. We use either the provided font, or 20 | // the Recursive font from the test folder 21 | font.src = process.argv[2] || "../fonts/Recursive_VF_1.064.ttf"; 22 | 23 | // Now we're ready to load the font and inspect it! 24 | font.onload = (evt) => { 25 | // Map the details LibFont gathered from the font to the 26 | // "font" variable 27 | const font = evt.detail.font; 28 | 29 | // From all the OpenType tables in the font, take the "name" 30 | // table so we can inspect it further 31 | const { name } = font.opentype.tables; 32 | 33 | // From the name table, take the entry with ID "1". This is 34 | // the Font Family name. More info and names you can grab: 35 | // https://docs.microsoft.com/en-us/typography/opentype/spec/name 36 | const fontname = name.get(1); 37 | 38 | // Tell us the name! 39 | console.log(`This font is called ${fontname}.`); 40 | } 41 | 42 | // If for some reason the font fails to load or parse, throw 43 | // an error 44 | font.onerror = (evt) => { 45 | console.error(evt.msg); 46 | } 47 | -------------------------------------------------------------------------------- /examples/introduction.js: -------------------------------------------------------------------------------- 1 | // This is the "introduction" example from LibFont's README 2 | 3 | // Import the LibFont library 4 | import { Font } from "../lib-font.js"; 5 | 6 | // Create a font object 7 | const myFont = new Font(`Adobe Source Code Pro`); 8 | 9 | // Assign event handling (.addEventListener version supported too, of course) 10 | myFont.onerror = evt => console.error(evt); 11 | myFont.onload = evt => doSomeFontThings(evt); 12 | 13 | // Kick off the font load by setting a source file, exactly as you would 14 | // for an or 7 | 8 | 9 | 10 | 11 |

Use LibFont in the browser

12 | 13 |

Note: you have to serve this from a 14 | local web server. 15 | It won't work if you open this file from the filesystem.

16 | 17 |

Load font:

18 | 19 |

20 | 21 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /fonts/AthenaRuby_b018.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/AthenaRuby_b018.ttf -------------------------------------------------------------------------------- /fonts/IBMPlexSansThai-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/IBMPlexSansThai-Light.ttf -------------------------------------------------------------------------------- /fonts/MehrNastaliqWeb-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/MehrNastaliqWeb-Regular.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-Light.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /fonts/OpenSans/OpenSans-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/OpenSans/OpenSans-SemiboldItalic.ttf -------------------------------------------------------------------------------- /fonts/README.md: -------------------------------------------------------------------------------- 1 | "Athena Ruby" font retrieved from https://www.doaks.org/resources/athena-ruby 2 | "Open Sans" fonts retrieved from https://www.opensans.com/ 3 | "Recursive" font retrieved from https://github.com/arrowtype/recursive 4 | "Source Code Pro" fonts retrieved from https://github.com/adobe-fonts/source-code-pro 5 | "Mehr Nastaliq" retrieved from https://github.com/simoncozens/nastaliq-engineering/tree/master/master_ttf (with permission based on https://twitter.com/ZeeshanNasar/status/1317546046071898112) 6 | "IBM Plex Sans Thai" retrieved from https://fonts.google.com/specimen/IBM+Plex+Sans+Thai" 7 | 8 | -------------------------------------------------------------------------------- /fonts/Recursive_VF_1.064.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/Recursive_VF_1.064.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodePro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodePro-Regular.otf -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodePro-Regular.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodePro-Regular.otf.woff -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodePro-Regular.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodePro-Regular.otf.woff2 -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodePro-Regular.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodePro-Regular.ttf.woff -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodePro-Regular.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodePro-Regular.ttf.woff2 -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodeVariable-Roman.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodeVariable-Roman.otf -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodeVariable-Roman.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodeVariable-Roman.otf.woff -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodeVariable-Roman.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodeVariable-Roman.otf.woff2 -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodeVariable-Roman.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodeVariable-Roman.ttf -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodeVariable-Roman.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodeVariable-Roman.ttf.woff -------------------------------------------------------------------------------- /fonts/SourceCodePro/SourceCodeVariable-Roman.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/SourceCodePro/SourceCodeVariable-Roman.ttf.woff2 -------------------------------------------------------------------------------- /fonts/issue-114/Flaticon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-114/Flaticon.woff2 -------------------------------------------------------------------------------- /fonts/issue-114/README.md: -------------------------------------------------------------------------------- 1 | See https://github.com/Pomax/lib-font/issues/114 for details. 2 | -------------------------------------------------------------------------------- /fonts/issue-114/roboto-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-114/roboto-regular.woff2 -------------------------------------------------------------------------------- /fonts/issue-114/tawk-font-icon-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-114/tawk-font-icon-2.woff2 -------------------------------------------------------------------------------- /fonts/issue-123/Castoro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-123/Castoro-Regular.woff2 -------------------------------------------------------------------------------- /fonts/issue-127/Abelone-FREE.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-127/Abelone-FREE.otf -------------------------------------------------------------------------------- /fonts/issue-130/ABeeZee-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-130/ABeeZee-Regular.ttf -------------------------------------------------------------------------------- /fonts/issue-130/AbyssinicaSIL-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-130/AbyssinicaSIL-Regular.ttf -------------------------------------------------------------------------------- /fonts/issue-130/AkayaKanadaka-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-130/AkayaKanadaka-Regular.ttf -------------------------------------------------------------------------------- /fonts/issue-130/Alice-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/lib-font/ebb3706649e9accfc8ac8df8d239dac3c167cd99/fonts/issue-130/Alice-Regular.ttf -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LibFont unit tests 6 | 7 | 8 | 9 | 10 | 22 | 23 | 24 |

Test Results

25 |

Back to index.html

26 | 27 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib-font", 3 | "version": "2.4.3", 4 | "description": "A JS based OpenType font inspector", 5 | "main": "./lib-font.js", 6 | "exports": "./lib-font.js", 7 | "type": "module", 8 | "author": "Pomax", 9 | "license": "LICENSE.md", 10 | "directories": { 11 | "lib": "lib", 12 | "src": "src" 13 | }, 14 | "scripts": { 15 | "bundle": "run-s rollup terser", 16 | "clean": "prettier --write \"src/**/*.js\"", 17 | "rollup": "rollup -i lib-font.js -n Font -o lib-font.browser.js --inlineDynamicImports --external fs,zlib", 18 | "start": "npm test", 19 | "terser": "terser lib-font.browser.js -o lib-font.browser.js", 20 | "test:browser": "run-p test:server test:puppeteer", 21 | "test:jest:node": "npm run test:jest -- ./testing/node/", 22 | "test:jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --verbose=false", 23 | "test:manual": "http-server -o testing/manual/index.html", 24 | "test:puppeteer": "node ./testing/browser/puppeteer.js", 25 | "test:server": "node ./testing/browser/server.js", 26 | "test": "run-s clean test:jest:node bundle test:browser" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/Pomax/lib-font.git" 31 | }, 32 | "keywords": [ 33 | "opentype", 34 | "font", 35 | "webfont", 36 | "parse", 37 | "inspect", 38 | "debug" 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/Pomax/lib-font/issues" 42 | }, 43 | "homepage": "https://github.com/Pomax/lib-font#readme", 44 | "devDependencies": { 45 | "cross-env": "^7.0.2", 46 | "express": "^4.17.1", 47 | "http-server": "^0.12.3", 48 | "jest": "^26.6.3", 49 | "npm-run-all": "^4.1.5", 50 | "open-cli": "^6.0.1", 51 | "prettier": "^2.1.1", 52 | "puppeteer": "^5.3.1", 53 | "rollup": "^2.53.0", 54 | "terser": "^5.7.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/eventing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple event manager so people can write the 3 | * same code in the browser and in Node. 4 | */ 5 | class Event { 6 | constructor(type, detail = {}, msg) { 7 | this.type = type; 8 | this.detail = detail; 9 | this.msg = msg; 10 | Object.defineProperty(this, `__mayPropagate`, { 11 | enumerable: false, 12 | writable: true, 13 | }); 14 | this.__mayPropagate = true; 15 | } 16 | preventDefault() { 17 | /* does nothing */ 18 | } 19 | stopPropagation() { 20 | this.__mayPropagate = false; 21 | } 22 | valueOf() { 23 | return this; 24 | } 25 | toString() { 26 | return this.msg 27 | ? `[${this.type} event]: ${this.msg}` 28 | : `[${this.type} event]`; 29 | } 30 | } 31 | 32 | /** 33 | * Simple event manager so people can write the 34 | * same code in the browser and in Node. 35 | */ 36 | class EventManager { 37 | constructor() { 38 | this.listeners = {}; 39 | } 40 | addEventListener(type, listener, useCapture) { 41 | let bin = this.listeners[type] || []; 42 | if (useCapture) bin.unshift(listener); 43 | else bin.push(listener); 44 | this.listeners[type] = bin; 45 | } 46 | removeEventListener(type, listener) { 47 | let bin = this.listeners[type] || []; 48 | let pos = bin.findIndex((e) => e === listener); 49 | if (pos > -1) { 50 | bin.splice(pos, 1); 51 | this.listeners[type] = bin; 52 | } 53 | } 54 | dispatch(event) { 55 | let bin = this.listeners[event.type]; 56 | if (bin) { 57 | for (let l = 0, e = bin.length; l < e; l++) { 58 | if (!event.__mayPropagate) break; 59 | bin[l](event); 60 | } 61 | } 62 | } 63 | } 64 | 65 | export { Event, EventManager }; 66 | -------------------------------------------------------------------------------- /src/lazy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a lazy loader but is not optimised for direct record selection, 3 | * so code will currently load "An entire array" even if it needs only 4 | * a single element from that array, and the array elements are fixed width. 5 | * 6 | * @param {*} object 7 | * @param {*} property 8 | * @param {*} getter 9 | */ 10 | export default function lazy(object, property, getter) { 11 | let val; 12 | Object.defineProperty(object, property, { 13 | get: () => { 14 | if (val) return val; 15 | val = getter(); 16 | return val; 17 | }, 18 | enumerable: true, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/opentype/index.js: -------------------------------------------------------------------------------- 1 | import { SFNT } from "./sfnt.js"; 2 | import { WOFF } from "./woff.js"; 3 | import { WOFF2 } from "./woff2.js"; 4 | export { SFNT, WOFF, WOFF2 }; 5 | -------------------------------------------------------------------------------- /src/opentype/sfnt.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "./tables/simple-table.js"; 2 | import lazy from "../lazy.js"; 3 | 4 | /** 5 | * the SFNT header. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/overview for more information 8 | */ 9 | class SFNT extends SimpleTable { 10 | constructor(font, dataview, createTable) { 11 | const { p } = super({ offset: 0, length: 12 }, dataview, `sfnt`); 12 | 13 | this.version = p.uint32; 14 | this.numTables = p.uint16; 15 | this.searchRange = p.uint16; 16 | this.entrySelector = p.uint16; 17 | this.rangeShift = p.uint16; 18 | 19 | p.verifyLength(); 20 | 21 | this.directory = [...new Array(this.numTables)].map( 22 | (_) => new TableRecord(p) 23 | ); 24 | 25 | // add convenience bindings for each table, with lazy loading 26 | this.tables = {}; 27 | this.directory.forEach((entry) => { 28 | const getter = () => { 29 | return createTable( 30 | this.tables, 31 | { 32 | tag: entry.tag, 33 | offset: entry.offset, 34 | length: entry.length, 35 | }, 36 | dataview 37 | ); 38 | }; 39 | lazy(this.tables, entry.tag.trim(), getter); 40 | }); 41 | } 42 | } 43 | 44 | /** 45 | * SFNT directory Table Record struct. 46 | */ 47 | class TableRecord { 48 | constructor(p) { 49 | this.tag = p.tag; 50 | this.checksum = p.uint32; 51 | this.offset = p.uint32; 52 | this.length = p.uint32; 53 | } 54 | } 55 | 56 | export { SFNT }; 57 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/BASE.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../lazy.js"; 2 | import { SimpleTable } from "../simple-table.js"; 3 | 4 | /** 5 | * The OpenType `BASE` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/BASE 8 | */ 9 | class BASE extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | 13 | this.majorVersion = p.uint16; 14 | this.minorVersion = p.uint16; 15 | this.horizAxisOffset = p.Offset16; // from beginning of BASE table 16 | this.vertAxisOffset = p.Offset16; // from beginning of BASE table 17 | 18 | lazy( 19 | this, 20 | `horizAxis`, 21 | () => 22 | new AxisTable({ offset: dict.offset + this.horizAxisOffset }, dataview) 23 | ); 24 | lazy( 25 | this, 26 | `vertAxis`, 27 | () => 28 | new AxisTable({ offset: dict.offset + this.vertAxisOffset }, dataview) 29 | ); 30 | 31 | if (this.majorVersion === 1 && this.minorVersion === 1) { 32 | this.itemVarStoreOffset = p.Offset32; // from beginning of BASE table 33 | lazy( 34 | this, 35 | `itemVarStore`, 36 | () => 37 | new AxisTable( 38 | { offset: dict.offset + this.itemVarStoreOffset }, 39 | dataview 40 | ) 41 | ); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Axis table 48 | */ 49 | class AxisTable extends SimpleTable { 50 | constructor(dict, dataview) { 51 | const { p } = super(dict, dataview, `AxisTable`); 52 | 53 | this.baseTagListOffset = p.Offset16; // from beginning of Axis table 54 | this.baseScriptListOffset = p.Offset16; // from beginning of Axis table 55 | 56 | lazy( 57 | this, 58 | `baseTagList`, 59 | () => 60 | new BaseTagListTable( 61 | { offset: dict.offset + this.baseTagListOffset }, 62 | dataview 63 | ) 64 | ); 65 | lazy( 66 | this, 67 | `baseScriptList`, 68 | () => 69 | new BaseScriptListTable( 70 | { offset: dict.offset + this.baseScriptListOffset }, 71 | dataview 72 | ) 73 | ); 74 | } 75 | } 76 | 77 | class BaseTagListTable extends SimpleTable { 78 | constructor(dict, dataview) { 79 | const { p } = super(dict, dataview, `BaseTagListTable`); 80 | this.baseTagCount = p.uint16; 81 | // TODO: make lazy? 82 | this.baselineTags = [...new Array(this.baseTagCount)].map((_) => p.tag); 83 | } 84 | } 85 | 86 | class BaseScriptListTable extends SimpleTable { 87 | constructor(dict, dataview) { 88 | const { p } = super(dict, dataview, `BaseScriptListTable`); 89 | this.baseScriptCount = p.uint16; 90 | 91 | const recordStart = p.currentPosition; 92 | lazy(this, `baseScriptRecords`, () => { 93 | p.currentPosition = recordStart; 94 | return [...new Array(this.baseScriptCount)].map( 95 | (_) => new BaseScriptRecord(this.start, p) 96 | ); 97 | }); 98 | } 99 | } 100 | 101 | class BaseScriptRecord { 102 | constructor(baseScriptListTableStart, p) { 103 | this.baseScriptTag = p.tag; 104 | this.baseScriptOffset = p.Offset16; // from beginning of BaseScriptList 105 | lazy(this, `baseScriptTable`, () => { 106 | p.currentPosition = baseScriptListTableStart + this.baseScriptOffset; 107 | return new BaseScriptTable(p); 108 | }); 109 | } 110 | } 111 | 112 | class BaseScriptTable { 113 | constructor(p) { 114 | this.start = p.currentPosition; 115 | this.baseValuesOffset = p.Offset16; // from beginning of BaseScript table 116 | this.defaultMinMaxOffset = p.Offset16; // from beginning of BaseScript table 117 | this.baseLangSysCount = p.uint16; 118 | this.baseLangSysRecords = [...new Array(this.baseLangSysCount)].map( 119 | (_) => new BaseLangSysRecord(this.start, p) 120 | ); 121 | 122 | lazy(this, `baseValues`, () => { 123 | p.currentPosition = this.start + this.baseValuesOffset; 124 | return new BaseValuesTable(p); 125 | }); 126 | 127 | lazy(this, `defaultMinMax`, () => { 128 | p.currentPosition = this.start + this.defaultMinMaxOffset; 129 | return new MinMaxTable(p); 130 | }); 131 | } 132 | } 133 | 134 | class BaseLangSysRecord { 135 | constructor(baseScriptTableStart, p) { 136 | this.baseLangSysTag = p.tag; 137 | this.minMaxOffset = p.Offset16; // from beginning of BaseScript table 138 | lazy(this, `minMax`, () => { 139 | p.currentPosition = baseScriptTableStart + this.minMaxOffset; 140 | return new MinMaxTable(p); 141 | }); 142 | } 143 | } 144 | 145 | class BaseValuesTable { 146 | constructor(p) { 147 | this.parser = p; 148 | this.start = p.currentPosition; 149 | 150 | this.defaultBaselineIndex = p.uint16; 151 | this.baseCoordCount = p.uint16; 152 | this.baseCoords = [...new Array(this.baseCoordCount)].map( 153 | (_) => p.Offset16 154 | ); 155 | } 156 | getTable(id) { 157 | this.parser.currentPosition = this.start + this.baseCoords[id]; 158 | return new BaseCoordTable(this.parser); 159 | } 160 | } 161 | 162 | class MinMaxTable { 163 | constructor(p) { 164 | this.minCoord = p.Offset16; 165 | this.maxCoord = p.Offset16; 166 | this.featMinMaxCount = p.uint16; 167 | 168 | const recordStart = p.currentPosition; 169 | lazy(this, `featMinMaxRecords`, () => { 170 | p.currentPosition = recordStart; 171 | return [...new Array(this.featMinMaxCount)].map( 172 | (_) => new FeatMinMaxRecord(p) 173 | ); 174 | }); 175 | } 176 | } 177 | 178 | class FeatMinMaxRecord { 179 | constructor(p) { 180 | this.featureTableTag = p.tag; 181 | this.minCoord = p.Offset16; 182 | this.maxCoord = p.Offset16; 183 | } 184 | } 185 | 186 | class BaseCoordTable { 187 | constructor(p) { 188 | this.baseCoordFormat = p.uint16; 189 | this.coordinate = p.int16; 190 | if (this.baseCoordFormat === 2) { 191 | this.referenceGlyph = p.uint16; 192 | this.baseCoordPoint = p.uint16; 193 | } 194 | if (this.baseCoordFormat === 3) { 195 | this.deviceTable = p.Offset16; 196 | } 197 | } 198 | } 199 | 200 | export { BASE }; 201 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/GDEF.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../parser.js"; 2 | import { SimpleTable } from "../simple-table.js"; 3 | import { ClassDefinition } from "./shared/class.js"; 4 | import { CoverageTable } from "./shared/coverage.js"; 5 | import { ItemVariationStoreTable } from "./shared/itemvariation.js"; 6 | import lazy from "../../../lazy.js"; 7 | 8 | /** 9 | * The OpenType `GDEF` table. 10 | * 11 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/GDEF 12 | */ 13 | class GDEF extends SimpleTable { 14 | constructor(dict, dataview) { 15 | const { p } = super(dict, dataview); 16 | 17 | // there are three possible versions 18 | this.majorVersion = p.uint16; 19 | this.minorVersion = p.uint16; 20 | 21 | this.glyphClassDefOffset = p.Offset16; 22 | lazy(this, `glyphClassDefs`, () => { 23 | if (this.glyphClassDefOffset === 0) return undefined; 24 | p.currentPosition = this.tableStart + this.glyphClassDefOffset; 25 | return new ClassDefinition(p); 26 | }); 27 | 28 | this.attachListOffset = p.Offset16; 29 | lazy(this, `attachList`, () => { 30 | if (this.attachListOffset === 0) return undefined; 31 | p.currentPosition = this.tableStart + this.attachListOffset; 32 | return new AttachList(p); 33 | }); 34 | 35 | this.ligCaretListOffset = p.Offset16; 36 | lazy(this, `ligCaretList`, () => { 37 | if (this.ligCaretListOffset === 0) return undefined; 38 | p.currentPosition = this.tableStart + this.ligCaretListOffset; 39 | return new LigCaretList(p); 40 | }); 41 | 42 | this.markAttachClassDefOffset = p.Offset16; 43 | lazy(this, `markAttachClassDef`, () => { 44 | if (this.markAttachClassDefOffset === 0) return undefined; 45 | p.currentPosition = this.tableStart + this.markAttachClassDefOffset; 46 | return new ClassDefinition(p); 47 | }); 48 | 49 | if (this.minorVersion >= 2) { 50 | this.markGlyphSetsDefOffset = p.Offset16; 51 | lazy(this, `markGlyphSetsDef`, () => { 52 | if (this.markGlyphSetsDefOffset === 0) return undefined; 53 | p.currentPosition = this.tableStart + this.markGlyphSetsDefOffset; 54 | return new MarkGlyphSetsTable(p); 55 | }); 56 | } 57 | 58 | if (this.minorVersion === 3) { 59 | this.itemVarStoreOffset = p.Offset32; 60 | lazy(this, `itemVarStore`, () => { 61 | if (this.itemVarStoreOffset === 0) return undefined; 62 | p.currentPosition = this.tableStart + this.itemVarStoreOffset; 63 | return new ItemVariationStoreTable(p); 64 | }); 65 | } 66 | } 67 | } 68 | 69 | class AttachList extends ParsedData { 70 | constructor(p) { 71 | super(p); 72 | this.coverageOffset = p.Offset16; // Offset to Coverage table - from beginning of AttachList table 73 | this.glyphCount = p.uint16; 74 | this.attachPointOffsets = [...new Array(this.glyphCount)].map( 75 | (_) => p.Offset16 76 | ); // From beginning of AttachList table (in Coverage Index order) 77 | } 78 | getPoint(pointID) { 79 | this.parser.currentPosition = this.start + this.attachPointOffsets[pointID]; 80 | return new AttachPoint(this.parser); 81 | } 82 | } 83 | 84 | class AttachPoint { 85 | constructor(p) { 86 | this.pointCount = p.uint16; 87 | this.pointIndices = [...new Array(this.pointCount)].map((_) => p.uint16); 88 | } 89 | } 90 | 91 | class LigCaretList extends ParsedData { 92 | constructor(p) { 93 | super(p); 94 | 95 | this.coverageOffset = p.Offset16; 96 | 97 | lazy(this, `coverage`, () => { 98 | p.currentPosition = this.start + this.coverageOffset; 99 | return new CoverageTable(p); 100 | }); 101 | 102 | this.ligGlyphCount = p.uint16; 103 | this.ligGlyphOffsets = [...new Array(this.ligGlyphCount)].map( 104 | (_) => p.Offset16 105 | ); // From beginning of LigCaretList table 106 | } 107 | 108 | getLigGlyph(ligGlyphID) { 109 | this.parser.currentPosition = this.start + this.ligGlyphOffsets[ligGlyphID]; 110 | return new LigGlyph(this.parser); 111 | } 112 | } 113 | 114 | class LigGlyph extends ParsedData { 115 | constructor(p) { 116 | super(p); 117 | this.caretCount = p.uint16; 118 | this.caretValueOffsets = [...new Array(this.caretCount)].map( 119 | (_) => p.Offset16 120 | ); // From beginning of LigGlyph table 121 | } 122 | 123 | getCaretValue(caretID) { 124 | this.parser.currentPosition = this.start + this.caretValueOffsets[caretID]; 125 | return new CaretValue(this.parser); 126 | } 127 | } 128 | 129 | class CaretValue { 130 | constructor(p) { 131 | this.caretValueFormat = p.uint16; 132 | 133 | if (this.caretValueFormat === 1) { 134 | this.coordinate = p.int16; 135 | } 136 | 137 | if (this.caretValueFormat === 2) { 138 | this.caretValuePointIndex = p.uint16; 139 | } 140 | 141 | if (this.caretValueFormat === 3) { 142 | this.coordinate = p.int16; 143 | this.deviceOffset = p.Offset16; // Offset to Device table (non-variable font) / Variation Index table (variable font) for X or Y value-from beginning of CaretValue table 144 | } 145 | } 146 | } 147 | 148 | class MarkGlyphSetsTable extends ParsedData { 149 | constructor(p) { 150 | super(p); 151 | 152 | this.markGlyphSetTableFormat = p.uint16; 153 | this.markGlyphSetCount = p.uint16; 154 | this.coverageOffsets = [...new Array(this.markGlyphSetCount)].map( 155 | (_) => p.Offset32 156 | ); 157 | } 158 | 159 | getMarkGlyphSet(markGlyphSetID) { 160 | this.parser.currentPosition = 161 | this.start + this.coverageOffsets[markGlyphSetID]; 162 | return new CoverageTable(this.parser); 163 | } 164 | } 165 | 166 | export { GDEF }; 167 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/GPOS.js: -------------------------------------------------------------------------------- 1 | import { CommonLayoutTable } from "../common-layout-table.js"; 2 | 3 | class GPOS extends CommonLayoutTable { 4 | constructor(dict, dataview) { 5 | super(dict, dataview, `GPOS`); 6 | } 7 | getLookup(lookupIndex) { 8 | return super.getLookup(lookupIndex, `GPOS`); 9 | } 10 | } 11 | 12 | export { GPOS }; 13 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/GSUB.js: -------------------------------------------------------------------------------- 1 | import { CommonLayoutTable } from "../common-layout-table.js"; 2 | 3 | class GSUB extends CommonLayoutTable { 4 | constructor(dict, dataview) { 5 | super(dict, dataview, `GSUB`); 6 | } 7 | 8 | getLookup(lookupIndex) { 9 | return super.getLookup(lookupIndex, `GSUB`); 10 | } 11 | } 12 | 13 | export { GSUB }; 14 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/JSTF.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../lazy.js"; 2 | import { SimpleTable } from "../simple-table.js"; 3 | 4 | /** 5 | * The OpenType `JSTF` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/JSTF 8 | */ 9 | class JSTF extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | } 13 | } 14 | 15 | export { JSTF }; 16 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/MATH.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../lazy.js"; 2 | import { SimpleTable } from "../simple-table.js"; 3 | 4 | /** 5 | * The OpenType `MATH` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/MATH 8 | */ 9 | class MATH extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | } 13 | } 14 | 15 | export { MATH }; 16 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/README.md: -------------------------------------------------------------------------------- 1 | See https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2 -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/gpos-lookup.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../../parser.js"; 2 | 3 | // Dummy placeholder 4 | class LookupType extends ParsedData { 5 | constructor(p) { 6 | super(p); 7 | } 8 | } 9 | 10 | export { LookupType }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-1.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType1 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 1`); 7 | } 8 | } 9 | 10 | export { LookupType1 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-2.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType2 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 2`); 7 | } 8 | } 9 | 10 | export { LookupType2 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-3.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType3 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 3`); 7 | } 8 | } 9 | 10 | export { LookupType3 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-4.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType4 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 4`); 7 | } 8 | } 9 | 10 | export { LookupType4 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-5.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType5 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 5`); 7 | } 8 | } 9 | 10 | export { LookupType5 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-6.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType6 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 6`); 7 | } 8 | } 9 | 10 | export { LookupType6 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-7.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType7 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 7`); 7 | } 8 | } 9 | 10 | export { LookupType7 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-8.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType8 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 8`); 7 | } 8 | } 9 | 10 | export { LookupType8 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gpos/lookup-type-9.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gpos-lookup.js"; 2 | 3 | class LookupType9 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | console.log(`lookup type 9`); 7 | } 8 | } 9 | 10 | export { LookupType9 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/gsub-lookup.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../../parser.js"; 2 | import { CoverageTable } from "../../shared/coverage.js"; 3 | 4 | function undoCoverageOffsetParsing(instance) { 5 | instance.parser.currentPosition -= 2; 6 | delete instance.coverageOffset; 7 | delete instance.getCoverageTable; 8 | } 9 | 10 | class LookupType extends ParsedData { 11 | constructor(p) { 12 | super(p); 13 | this.substFormat = p.uint16; 14 | this.coverageOffset = p.Offset16; 15 | } 16 | getCoverageTable() { 17 | let p = this.parser; 18 | p.currentPosition = this.start + this.coverageOffset; 19 | return new CoverageTable(p); 20 | } 21 | } 22 | 23 | // used by types 5 and 6 24 | class SubstLookupRecord { 25 | constructor(p) { 26 | this.glyphSequenceIndex = p.uint16; // Index into current glyph sequence — first glyph = 0. 27 | this.lookupListIndex = p.uint16; // Lookup to apply to that position — zero-based index. 28 | } 29 | } 30 | 31 | export { undoCoverageOffsetParsing, LookupType, SubstLookupRecord }; 32 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-1.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gsub-lookup.js"; 2 | 3 | class LookupType1 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | this.deltaGlyphID = p.int16; 7 | } 8 | } 9 | 10 | export { LookupType1 }; 11 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-2.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gsub-lookup.js"; 2 | 3 | class LookupType2 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | this.sequenceCount = p.uint16; 7 | this.sequenceOffsets = [...new Array(this.sequenceCount)].map( 8 | (_) => p.Offset16 9 | ); 10 | } 11 | getSequence(index) { 12 | let p = this.parser; 13 | p.currentPosition = this.start + this.sequenceOffsets[index]; 14 | return new SequenceTable(p); 15 | } 16 | } 17 | 18 | class SequenceTable { 19 | constructor(p) { 20 | this.glyphCount = p.uint16; 21 | this.substituteGlyphIDs = [...new Array(this.glyphCount)].map( 22 | (_) => p.uint16 23 | ); 24 | } 25 | } 26 | 27 | export { LookupType2 }; 28 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-3.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gsub-lookup.js"; 2 | 3 | class LookupType3 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | this.alternateSetCount = p.uint16; 7 | this.alternateSetOffsets = [...new Array(this.alternateSetCount)].map( 8 | (_) => p.Offset16 9 | ); 10 | } 11 | getAlternateSet(index) { 12 | let p = this.parser; 13 | p.currentPosition = this.start + this.alternateSetOffsets[index]; 14 | return new AlternateSetTable(p); 15 | } 16 | } 17 | 18 | class AlternateSetTable { 19 | constructor(p) { 20 | this.glyphCount = p.uint16; 21 | this.alternateGlyphIDs = [...new Array(this.glyphCount)].map( 22 | (_) => p.uint16 23 | ); 24 | } 25 | } 26 | 27 | export { LookupType3 }; 28 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-4.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../../parser.js"; 2 | import { LookupType } from "./gsub-lookup.js"; 3 | 4 | class LookupType4 extends LookupType { 5 | constructor(p) { 6 | super(p); 7 | this.ligatureSetCount = p.uint16; 8 | this.ligatureSetOffsets = [...new Array(this.ligatureSetCount)].map( 9 | (_) => p.Offset16 10 | ); // from beginning of subtable 11 | } 12 | getLigatureSet(index) { 13 | let p = this.parser; 14 | p.currentPosition = this.start + this.ligatureSetOffsets[index]; 15 | return new LigatureSetTable(p); 16 | } 17 | } 18 | 19 | class LigatureSetTable extends ParsedData { 20 | constructor(p) { 21 | super(p); 22 | this.ligatureCount = p.uint16; 23 | this.ligatureOffsets = [...new Array(this.ligatureCount)].map( 24 | (_) => p.Offset16 25 | ); // from beginning of LigatureSetTable 26 | } 27 | getLigature(index) { 28 | let p = this.parser; 29 | p.currentPosition = this.start + this.ligatureOffsets[index]; 30 | return new LigatureTable(p); 31 | } 32 | } 33 | 34 | class LigatureTable { 35 | constructor(p) { 36 | this.ligatureGlyph = p.uint16; 37 | this.componentCount = p.uint16; 38 | this.componentGlyphIDs = [...new Array(this.componentCount - 1)].map( 39 | (_) => p.uint16 40 | ); 41 | } 42 | } 43 | 44 | export { LookupType4 }; 45 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-5.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../../parser.js"; 2 | import { 3 | LookupType, 4 | undoCoverageOffsetParsing, 5 | SubstLookupRecord, 6 | } from "./gsub-lookup.js"; 7 | import { CoverageTable } from "../../shared/coverage.js"; 8 | 9 | class LookupType5 extends LookupType { 10 | constructor(p) { 11 | super(p); 12 | 13 | // There are three possible subtable formats 14 | 15 | if (this.substFormat === 1) { 16 | this.subRuleSetCount = p.uint16; 17 | this.subRuleSetOffsets = [...new Array(this.subRuleSetCount)].map( 18 | (_) => p.Offset16 19 | ); 20 | } 21 | 22 | if (this.substFormat === 2) { 23 | this.classDefOffset = p.Offset16; 24 | this.subClassSetCount = p.uint16; 25 | this.subClassSetOffsets = [...new Array(this.subClassSetCount)].map( 26 | (_) => p.Offset16 27 | ); 28 | } 29 | 30 | if (this.substFormat === 3) { 31 | // undo the coverageOffset parsing, because this format uses an 32 | // entire *array* of coverage offsets instead, like 6.3 33 | undoCoverageOffsetParsing(this); 34 | 35 | this.glyphCount = p.uint16; 36 | this.substitutionCount = p.uint16; 37 | this.coverageOffsets = [...new Array(this.glyphCount)].map( 38 | (_) => p.Offset16 39 | ); 40 | this.substLookupRecords = [...new Array(this.substitutionCount)].map( 41 | (_) => new SubstLookupRecord(p) 42 | ); 43 | } 44 | } 45 | 46 | getSubRuleSet(index) { 47 | if (this.substFormat !== 1) 48 | throw new Error(`lookup type 5.${this.substFormat} has no subrule sets.`); 49 | let p = this.parser; 50 | p.currentPosition = this.start + this.subRuleSetOffsets[index]; 51 | return new SubRuleSetTable(p); 52 | } 53 | 54 | getSubClassSet(index) { 55 | if (this.substFormat !== 2) 56 | throw new Error( 57 | `lookup type 5.${this.substFormat} has no subclass sets.` 58 | ); 59 | let p = this.parser; 60 | p.currentPosition = this.start + this.subClassSetOffsets[index]; 61 | return new SubClassSetTable(p); 62 | } 63 | 64 | getCoverageTable(index) { 65 | if (this.substFormat !== 3 && !index) return super.getCoverageTable(); 66 | 67 | if (!index) 68 | throw new Error( 69 | `lookup type 5.${this.substFormat} requires an coverage table index.` 70 | ); 71 | 72 | let p = this.parser; 73 | p.currentPosition = this.start + this.coverageOffsets[index]; 74 | return new CoverageTable(p); 75 | } 76 | } 77 | 78 | // 5.1 79 | 80 | class SubRuleSetTable extends ParsedData { 81 | constructor(p) { 82 | super(p); 83 | this.subRuleCount = p.uint16; 84 | this.subRuleOffsets = [...new Array(this.subRuleCount)].map( 85 | (_) => p.Offset16 86 | ); 87 | } 88 | getSubRule(index) { 89 | let p = this.parser; 90 | p.currentPosition = this.start + this.subRuleOffsets[index]; 91 | return new SubRuleTable(p); 92 | } 93 | } 94 | 95 | class SubRuleTable { 96 | constructor(p) { 97 | this.glyphCount = p.uint16; 98 | this.substitutionCount = p.uint16; 99 | this.inputSequence = [...new Array(this.glyphCount - 1)].map( 100 | (_) => p.uint16 101 | ); 102 | this.substLookupRecords = [...new Array(this.substitutionCount)].map( 103 | (_) => new SubstLookupRecord(p) 104 | ); 105 | } 106 | } 107 | 108 | // 5.2 109 | 110 | class SubClassSetTable extends ParsedData { 111 | constructor(p) { 112 | super(p); 113 | this.subClassRuleCount = p.uint16; 114 | this.subClassRuleOffsets = [...new Array(this.subClassRuleCount)].map( 115 | (_) => p.Offset16 116 | ); 117 | } 118 | getSubClass(index) { 119 | let p = this.parser; 120 | p.currentPosition = this.start + this.subClassRuleOffsets[index]; 121 | return new SubClassRuleTable(p); 122 | } 123 | } 124 | 125 | class SubClassRuleTable extends SubRuleTable { 126 | constructor(p) { 127 | super(p); 128 | } 129 | } 130 | 131 | export { LookupType5 }; 132 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-6.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../../parser.js"; 2 | import { 3 | LookupType, 4 | undoCoverageOffsetParsing, 5 | SubstLookupRecord, 6 | } from "./gsub-lookup.js"; 7 | import { CoverageTable } from "../../shared/coverage.js"; 8 | 9 | class LookupType6 extends LookupType { 10 | constructor(p) { 11 | super(p); 12 | 13 | // There are three possible subtable formats 14 | 15 | if (this.substFormat === 1) { 16 | this.chainSubRuleSetCount = p.uint16; 17 | this.chainSubRuleSetOffsets = [ 18 | ...new Array(this.chainSubRuleSetCount), 19 | ].map((_) => p.Offset16); 20 | } 21 | 22 | if (this.substFormat === 2) { 23 | this.backtrackClassDefOffset = p.Offset16; 24 | this.inputClassDefOffset = p.Offset16; 25 | this.lookaheadClassDefOffset = p.Offset16; 26 | this.chainSubClassSetCount = p.uint16; 27 | this.chainSubClassSetOffsets = [ 28 | ...new Array(this.chainSubClassSetCount), 29 | ].map((_) => p.Offset16); 30 | } 31 | 32 | if (this.substFormat === 3) { 33 | // undo the coverageOffset parsing, because this format uses an 34 | // entire *array* of coverage offsets instead, like 5.3 35 | undoCoverageOffsetParsing(this); 36 | 37 | this.backtrackGlyphCount = p.uint16; 38 | this.backtrackCoverageOffsets = [ 39 | ...new Array(this.backtrackGlyphCount), 40 | ].map((_) => p.Offset16); 41 | this.inputGlyphCount = p.uint16; 42 | this.inputCoverageOffsets = [...new Array(this.inputGlyphCount)].map( 43 | (_) => p.Offset16 44 | ); 45 | this.lookaheadGlyphCount = p.uint16; 46 | this.lookaheadCoverageOffsets = [ 47 | ...new Array(this.lookaheadGlyphCount), 48 | ].map((_) => p.Offset16); 49 | this.seqLookupCount = p.uint16; 50 | this.seqLookupRecords = [...new Array(this.substitutionCount)].map( 51 | (_) => new SequenceLookupRecord(p) 52 | ); 53 | } 54 | } 55 | 56 | getChainSubRuleSet(index) { 57 | if (this.substFormat !== 1) 58 | throw new Error( 59 | `lookup type 6.${this.substFormat} has no chainsubrule sets.` 60 | ); 61 | let p = this.parser; 62 | p.currentPosition = this.start + this.chainSubRuleSetOffsets[index]; 63 | return new ChainSubRuleSetTable(p); 64 | } 65 | 66 | getChainSubClassSet(index) { 67 | if (this.substFormat !== 2) 68 | throw new Error( 69 | `lookup type 6.${this.substFormat} has no chainsubclass sets.` 70 | ); 71 | let p = this.parser; 72 | p.currentPosition = this.start + this.chainSubClassSetOffsets[index]; 73 | return new ChainSubClassSetTable(p); 74 | } 75 | 76 | getCoverageFromOffset(offset) { 77 | if (this.substFormat !== 3) 78 | throw new Error( 79 | `lookup type 6.${this.substFormat} does not use contextual coverage offsets.` 80 | ); 81 | let p = this.parser; 82 | p.currentPosition = this.start + offset; 83 | return new CoverageTable(p); 84 | } 85 | } 86 | 87 | // 6.1 88 | 89 | class ChainSubRuleSetTable extends ParsedData { 90 | constructor(p) { 91 | super(p); 92 | this.chainSubRuleCount = p.uint16; 93 | this.chainSubRuleOffsets = [...new Array(this.chainSubRuleCount)].map( 94 | (_) => p.Offset16 95 | ); 96 | } 97 | getSubRule(index) { 98 | let p = this.parser; 99 | p.currentPosition = this.start + this.chainSubRuleOffsets[index]; 100 | return new ChainSubRuleTable(p); 101 | } 102 | } 103 | 104 | class ChainSubRuleTable { 105 | constructor(p) { 106 | this.backtrackGlyphCount = p.uint16; 107 | this.backtrackSequence = [...new Array(this.backtrackGlyphCount)].map( 108 | (_) => p.uint16 109 | ); 110 | this.inputGlyphCount = p.uint16; 111 | this.inputSequence = [...new Array(this.inputGlyphCount - 1)].map( 112 | (_) => p.uint16 113 | ); 114 | this.lookaheadGlyphCount = p.uint16; 115 | this.lookAheadSequence = [...new Array(this.lookAheadGlyphCount)].map( 116 | (_) => p.uint16 117 | ); 118 | this.substitutionCount = p.uint16; 119 | this.substLookupRecords = [...new Array(this.SubstCount)].map( 120 | (_) => new SubstLookupRecord(p) 121 | ); 122 | } 123 | } 124 | 125 | // 6.2 126 | 127 | class ChainSubClassSetTable extends ParsedData { 128 | constructor(p) { 129 | super(p); 130 | this.chainSubClassRuleCount = p.uint16; 131 | this.chainSubClassRuleOffsets = [ 132 | ...new Array(this.chainSubClassRuleCount), 133 | ].map((_) => p.Offset16); 134 | } 135 | getSubClass(index) { 136 | let p = this.parser; 137 | p.currentPosition = this.start + this.chainSubRuleOffsets[index]; 138 | return new ChainSubClassRuleTable(p); 139 | } 140 | } 141 | 142 | class ChainSubClassRuleTable { 143 | constructor(p) { 144 | this.backtrackGlyphCount = p.uint16; 145 | this.backtrackSequence = [...new Array(this.backtrackGlyphCount)].map( 146 | (_) => p.uint16 147 | ); 148 | this.inputGlyphCount = p.uint16; 149 | this.inputSequence = [...new Array(this.inputGlyphCount - 1)].map( 150 | (_) => p.uint16 151 | ); 152 | this.lookaheadGlyphCount = p.uint16; 153 | this.lookAheadSequence = [...new Array(this.lookAheadGlyphCount)].map( 154 | (_) => p.uint16 155 | ); 156 | this.substitutionCount = p.uint16; 157 | this.substLookupRecords = [...new Array(this.substitutionCount)].map( 158 | (_) => new SequenceLookupRecord(p) 159 | ); 160 | } 161 | } 162 | 163 | // 6.3 164 | 165 | class SequenceLookupRecord extends ParsedData { 166 | constructor(p) { 167 | super(p); 168 | this.sequenceIndex = p.uint16; 169 | this.lookupListIndex = p.uint16; 170 | } 171 | } 172 | 173 | export { LookupType6 }; 174 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-7.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../../parser.js"; 2 | 3 | class LookupType7 extends ParsedData { 4 | // note: not "extends LookupType" 5 | constructor(p) { 6 | super(p); 7 | this.substFormat = p.uint16; 8 | this.extensionLookupType = p.uint16; 9 | this.extensionOffset = p.Offset32; 10 | } 11 | } 12 | 13 | export { LookupType7 }; 14 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/lookups/gsub/lookup-type-8.js: -------------------------------------------------------------------------------- 1 | import { LookupType } from "./gsub-lookup.js"; 2 | 3 | class LookupType8 extends LookupType { 4 | constructor(p) { 5 | super(p); 6 | this.backtrackGlyphCount = p.uint16; 7 | this.backtrackCoverageOffsets = [ 8 | ...new Array(this.backtrackGlyphCount), 9 | ].map((_) => p.Offset16); 10 | this.lookaheadGlyphCount = p.uint16; 11 | this.lookaheadCoverageOffsets = [new Array(this.lookaheadGlyphCount)].map( 12 | (_) => p.Offset16 13 | ); 14 | this.glyphCount = p.uint16; 15 | this.substituteGlyphIDs = [...new Array(this.glyphCount)].map( 16 | (_) => p.uint16 17 | ); 18 | } 19 | } 20 | 21 | export { LookupType8 }; 22 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/class.js: -------------------------------------------------------------------------------- 1 | class ClassDefinition { 2 | constructor(p) { 3 | this.classFormat = p.uint16; 4 | 5 | if (this.classFormat === 1) { 6 | this.startGlyphID = p.uint16; 7 | this.glyphCount = p.uint16; 8 | this.classValueArray = [...new Array(this.glyphCount)].map( 9 | (_) => p.uint16 10 | ); 11 | } 12 | 13 | if (this.classFormat === 2) { 14 | this.classRangeCount = p.uint16; 15 | this.classRangeRecords = [...new Array(this.classRangeCount)].map( 16 | (_) => new ClassRangeRecord(p) 17 | ); 18 | } 19 | } 20 | } 21 | 22 | class ClassRangeRecord { 23 | constructor(p) { 24 | this.startGlyphID = p.uint16; 25 | this.endGlyphID = p.uint16; 26 | this.class = p.uint16; 27 | } 28 | } 29 | 30 | export { ClassDefinition }; 31 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/coverage.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../parser.js"; 2 | 3 | class CoverageTable extends ParsedData { 4 | constructor(p) { 5 | super(p); 6 | 7 | this.coverageFormat = p.uint16; 8 | 9 | if (this.coverageFormat === 1) { 10 | this.glyphCount = p.uint16; 11 | this.glyphArray = [...new Array(this.glyphCount)].map((_) => p.uint16); 12 | } 13 | 14 | if (this.coverageFormat === 2) { 15 | this.rangeCount = p.uint16; 16 | this.rangeRecords = [...new Array(this.rangeCount)].map( 17 | (_) => new CoverageRangeRecord(p) 18 | ); 19 | } 20 | } 21 | } 22 | 23 | class CoverageRangeRecord { 24 | constructor(p) { 25 | this.startGlyphID = p.uint16; 26 | this.endGlyphID = p.uint16; 27 | this.startCoverageIndex = p.uint16; 28 | } 29 | } 30 | 31 | export { CoverageTable }; 32 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/feature.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../parser.js"; 2 | 3 | class FeatureList extends ParsedData { 4 | static EMPTY = { 5 | featureCount: 0, 6 | featureRecords: [], 7 | }; 8 | 9 | constructor(p) { 10 | super(p); 11 | this.featureCount = p.uint16; 12 | this.featureRecords = [...new Array(this.featureCount)].map( 13 | (_) => new FeatureRecord(p) 14 | ); 15 | } 16 | } 17 | 18 | class FeatureRecord { 19 | constructor(p) { 20 | this.featureTag = p.tag; 21 | this.featureOffset = p.Offset16; // Offset to Feature table, from beginning of FeatureList 22 | } 23 | } 24 | 25 | class FeatureTable extends ParsedData { 26 | constructor(p) { 27 | super(p); 28 | this.featureParams = p.Offset16; 29 | this.lookupIndexCount = p.uint16; 30 | this.lookupListIndices = [...new Array(this.lookupIndexCount)].map( 31 | (_) => p.uint16 32 | ); 33 | // this.featureTag is imparted by the parser 34 | } 35 | 36 | // In order to parse the feature parameters, if there are any, we need to know which 37 | // feature this is, which is determined by the FeatureRecord.featureTag string. 38 | getFeatureParams() { 39 | if (this.featureParams > 0) { 40 | const p = this.parser; 41 | p.currentPosition = this.start + this.featureParams; 42 | const tag = this.featureTag; 43 | if (tag === `size`) return new Size(p); 44 | if (tag.startsWith(`cv`)) return new CharacterVariant(p); 45 | if (tag.startsWith(`ss`)) return new StylisticSet(p); 46 | } 47 | } 48 | } 49 | 50 | class CharacterVariant { 51 | // See https://docs.microsoft.com/en-us/typography/opentype/spec/features_ae#tag-cv01--cv99 52 | constructor(p) { 53 | this.format = p.uint16; 54 | this.featUiLabelNameId = p.uint16; 55 | this.featUiTooltipTextNameId = p.uint16; 56 | this.sampleTextNameId = p.uint16; 57 | this.numNamedParameters = p.uint16; 58 | this.firstParamUiLabelNameId = p.uint16; 59 | this.charCount = p.uint16; 60 | this.character = [...new Array(this.charCount)].map((_) => p.uint24); 61 | } 62 | } 63 | 64 | class Size { 65 | // See https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#-tag-size 66 | constructor(p) { 67 | this.designSize = p.uint16; 68 | this.subfamilyIdentifier = p.uint16; 69 | this.subfamilyNameID = p.uint16; 70 | this.smallEnd = p.uint16; 71 | this.largeEnd = p.uint16; 72 | } 73 | } 74 | 75 | class StylisticSet { 76 | // See https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#-tag-ss01---ss20 77 | constructor(p) { 78 | this.version = p.uint16; 79 | this.UINameID = p.uint16; 80 | } 81 | } 82 | 83 | export { FeatureList, FeatureTable }; 84 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/itemvariation.js: -------------------------------------------------------------------------------- 1 | class ItemVariationStoreTable { 2 | constructor(table, p) { 3 | this.table = table; 4 | this.parser = p; 5 | this.start = p.currentPosition; 6 | 7 | this.format = p.uint16; 8 | this.variationRegionListOffset = p.Offset32; 9 | this.itemVariationDataCount = p.uint16; 10 | this.itemVariationDataOffsets = [ 11 | ...new Array(this.itemVariationDataCount), 12 | ].map((_) => p.Offset32); 13 | } 14 | } 15 | 16 | class ItemVariationData { 17 | constructor(p) { 18 | this.itemCount = p.uint16; 19 | this.shortDeltaCount = p.uint16; 20 | this.regionIndexCount = p.uint16; 21 | this.regionIndexes = p.uint16; 22 | this.deltaSets = [...new Array(this.itemCount)].map( 23 | (_) => new DeltaSet(p, this.shortDeltaCount, this.regionIndexCount) 24 | ); 25 | } 26 | } 27 | 28 | class DeltaSet { 29 | constructor(p, shortDeltaCount, regionIndexCount) { 30 | // the documentation here seems problematic: 31 | // 32 | // "Logically, each DeltaSet record has regionIndexCount number of elements. 33 | // The first shortDeltaCount elements are represented as signed 16-bit values 34 | // (int16), and the remaining regionIndexCount - shortDeltaCount elements are 35 | // represented as signed 8-bit values (int8). The length of the data for each 36 | // row is shortDeltaCount + regionIndexCount." 37 | // 38 | // I'm assuming that should be "the remaining regionIndexCount elements are". 39 | this.DeltaData = []; 40 | while (shortDeltaCount-- > 0) { 41 | this.DeltaData.push(p.in16); 42 | } 43 | while (regionIndexCount-- > 0) { 44 | this.DeltaData.push(p.int8); 45 | } 46 | } 47 | } 48 | 49 | export { ItemVariationStoreTable }; 50 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/lookup.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../parser.js"; 2 | import GSUBtables from "./subtables/gsub.js"; 3 | import GPOStables from "./subtables/gpos.js"; 4 | 5 | class LookupList extends ParsedData { 6 | static EMPTY = { 7 | lookupCount: 0, 8 | lookups: [], 9 | }; 10 | 11 | constructor(p) { 12 | super(p); 13 | this.lookupCount = p.uint16; 14 | this.lookups = [...new Array(this.lookupCount)].map((_) => p.Offset16); // Array of offsets to Lookup tables, from beginning of LookupList 15 | } 16 | } 17 | 18 | class LookupTable extends ParsedData { 19 | constructor(p, type) { 20 | super(p); 21 | this.ctType = type; 22 | this.lookupType = p.uint16; 23 | this.lookupFlag = p.uint16; 24 | this.subTableCount = p.uint16; 25 | this.subtableOffsets = [...new Array(this.subTableCount)].map( 26 | (_) => p.Offset16 27 | ); // Array of offsets to lookup subtables, from beginning of Lookup table 28 | this.markFilteringSet = p.uint16; 29 | } 30 | get rightToLeft() { 31 | return this.lookupFlag & (0x0001 === 0x0001); 32 | } 33 | get ignoreBaseGlyphs() { 34 | return this.lookupFlag & (0x0002 === 0x0002); 35 | } 36 | get ignoreLigatures() { 37 | return this.lookupFlag & (0x0004 === 0x0004); 38 | } 39 | get ignoreMarks() { 40 | return this.lookupFlag & (0x0008 === 0x0008); 41 | } 42 | get useMarkFilteringSet() { 43 | return this.lookupFlag & (0x0010 === 0x0010); 44 | } 45 | get markAttachmentType() { 46 | return this.lookupFlag & (0xff00 === 0xff00); 47 | } 48 | 49 | // FIXME: make this a lazy .subtables array instead? 50 | getSubTable(index) { 51 | const builder = this.ctType === `GSUB` ? GSUBtables : GPOStables; 52 | this.parser.currentPosition = this.start + this.subtableOffsets[index]; 53 | return builder.buildSubtable(this.lookupType, this.parser); 54 | } 55 | } 56 | 57 | export { LookupList, LookupTable }; 58 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/script.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../parser.js"; 2 | 3 | /** 4 | * ... 5 | */ 6 | class ScriptList extends ParsedData { 7 | static EMPTY = { 8 | scriptCount: 0, 9 | scriptRecords: [], 10 | }; 11 | 12 | constructor(p) { 13 | super(p); 14 | this.scriptCount = p.uint16; 15 | this.scriptRecords = [...new Array(this.scriptCount)].map( 16 | (_) => new ScriptRecord(p) 17 | ); 18 | } 19 | } 20 | 21 | /** 22 | * ... 23 | */ 24 | class ScriptRecord { 25 | constructor(p) { 26 | this.scriptTag = p.tag; 27 | this.scriptOffset = p.Offset16; // Offset to Script table, from beginning of ScriptList 28 | } 29 | } 30 | 31 | /** 32 | * ... 33 | */ 34 | class ScriptTable extends ParsedData { 35 | constructor(p) { 36 | super(p); 37 | this.defaultLangSys = p.Offset16; // Offset to default LangSys table, from beginning of Script table — may be NULL 38 | this.langSysCount = p.uint16; 39 | this.langSysRecords = [...new Array(this.langSysCount)].map( 40 | (_) => new LangSysRecord(p) 41 | ); 42 | } 43 | } 44 | 45 | /** 46 | * ... 47 | */ 48 | class LangSysRecord { 49 | constructor(p) { 50 | this.langSysTag = p.tag; 51 | this.langSysOffset = p.Offset16; // Offset to LangSys table, from beginning of Script table 52 | } 53 | } 54 | 55 | /** 56 | * ... 57 | */ 58 | class LangSysTable { 59 | constructor(p) { 60 | this.lookupOrder = p.Offset16; 61 | this.requiredFeatureIndex = p.uint16; 62 | this.featureIndexCount = p.uint16; 63 | this.featureIndices = [...new Array(this.featureIndexCount)].map( 64 | (_) => p.uint16 65 | ); 66 | } 67 | } 68 | 69 | export { ScriptList, ScriptTable, LangSysTable }; 70 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/subtables/gpos.js: -------------------------------------------------------------------------------- 1 | import { LookupType1 } from "../../lookups/gpos/lookup-type-1.js"; 2 | import { LookupType2 } from "../../lookups/gpos/lookup-type-2.js"; 3 | import { LookupType3 } from "../../lookups/gpos/lookup-type-3.js"; 4 | import { LookupType4 } from "../../lookups/gpos/lookup-type-4.js"; 5 | import { LookupType5 } from "../../lookups/gpos/lookup-type-5.js"; 6 | import { LookupType6 } from "../../lookups/gpos/lookup-type-6.js"; 7 | import { LookupType7 } from "../../lookups/gpos/lookup-type-7.js"; 8 | import { LookupType8 } from "../../lookups/gpos/lookup-type-8.js"; 9 | import { LookupType9 } from "../../lookups/gpos/lookup-type-9.js"; 10 | 11 | export default { 12 | buildSubtable: function (type, p) { 13 | const subtable = new [ 14 | undefined, 15 | LookupType1, 16 | LookupType2, 17 | LookupType3, 18 | LookupType4, 19 | LookupType5, 20 | LookupType6, 21 | LookupType7, 22 | LookupType8, 23 | LookupType9, 24 | ][type](p); 25 | subtable.type = type; 26 | return subtable; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/opentype/tables/advanced/shared/subtables/gsub.js: -------------------------------------------------------------------------------- 1 | import { LookupType1 } from "../../lookups/gsub/lookup-type-1.js"; 2 | import { LookupType2 } from "../../lookups/gsub/lookup-type-2.js"; 3 | import { LookupType3 } from "../../lookups/gsub/lookup-type-3.js"; 4 | import { LookupType4 } from "../../lookups/gsub/lookup-type-4.js"; 5 | import { LookupType5 } from "../../lookups/gsub/lookup-type-5.js"; 6 | import { LookupType6 } from "../../lookups/gsub/lookup-type-6.js"; 7 | import { LookupType7 } from "../../lookups/gsub/lookup-type-7.js"; 8 | import { LookupType8 } from "../../lookups/gsub/lookup-type-8.js"; 9 | 10 | export default { 11 | buildSubtable: function (type, p) { 12 | const subtable = new [ 13 | undefined, 14 | LookupType1, 15 | LookupType2, 16 | LookupType3, 17 | LookupType4, 18 | LookupType5, 19 | LookupType6, 20 | LookupType7, 21 | LookupType8, 22 | ][type](p); 23 | subtable.type = type; 24 | return subtable; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/opentype/tables/common-layout-table.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "./simple-table.js"; 2 | import { 3 | ScriptList, 4 | ScriptTable, 5 | LangSysTable, 6 | } from "./advanced/shared/script.js"; 7 | import { FeatureList, FeatureTable } from "./advanced/shared/feature.js"; 8 | import { LookupList, LookupTable } from "./advanced/shared/lookup.js"; 9 | import lazy from "../../lazy.js"; 10 | 11 | /** 12 | * The table layout used by both GSUB and GPOS 13 | * 14 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/GSUB 15 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/GPOS 16 | */ 17 | class CommonLayoutTable extends SimpleTable { 18 | constructor(dict, dataview, name) { 19 | const { p, tableStart } = super(dict, dataview, name); 20 | 21 | this.majorVersion = p.uint16; 22 | this.minorVersion = p.uint16; 23 | this.scriptListOffset = p.Offset16; 24 | this.featureListOffset = p.Offset16; 25 | this.lookupListOffset = p.Offset16; 26 | 27 | if (this.majorVersion === 1 && this.minorVersion === 1) { 28 | this.featureVariationsOffset = p.Offset32; 29 | } 30 | 31 | const no_content = !( 32 | this.scriptListOffset || 33 | this.featureListOffset || 34 | this.lookupListOffset 35 | ); 36 | 37 | lazy(this, `scriptList`, () => { 38 | if (no_content) return ScriptList.EMPTY; 39 | p.currentPosition = tableStart + this.scriptListOffset; 40 | return new ScriptList(p); 41 | }); 42 | 43 | lazy(this, `featureList`, () => { 44 | if (no_content) return FeatureList.EMPTY; 45 | p.currentPosition = tableStart + this.featureListOffset; 46 | return new FeatureList(p); 47 | }); 48 | 49 | lazy(this, `lookupList`, () => { 50 | if (no_content) return LookupList.EMPTY; 51 | p.currentPosition = tableStart + this.lookupListOffset; 52 | return new LookupList(p); 53 | }); 54 | 55 | // FIXME: This class doesn't actually exist anywhere in the code... 56 | 57 | if (this.featureVariationsOffset) { 58 | lazy(this, `featureVariations`, () => { 59 | if (no_content) return FeatureVariations.EMPTY; 60 | p.currentPosition = tableStart + this.featureVariationsOffset; 61 | return new FeatureVariations(p); 62 | }); 63 | } 64 | } 65 | 66 | // Script functions 67 | 68 | getSupportedScripts() { 69 | return this.scriptList.scriptRecords.map((r) => r.scriptTag); 70 | } 71 | 72 | getScriptTable(scriptTag) { 73 | let record = this.scriptList.scriptRecords.find( 74 | (r) => r.scriptTag === scriptTag 75 | ); 76 | this.parser.currentPosition = this.scriptList.start + record.scriptOffset; 77 | let table = new ScriptTable(this.parser); 78 | table.scriptTag = scriptTag; 79 | return table; 80 | } 81 | 82 | // LangSys functions 83 | 84 | ensureScriptTable(arg) { 85 | if (typeof arg === "string") { 86 | return this.getScriptTable(arg); 87 | } 88 | return arg; 89 | } 90 | 91 | getSupportedLangSys(scriptTable) { 92 | scriptTable = this.ensureScriptTable(scriptTable); 93 | const hasDefault = scriptTable.defaultLangSys !== 0; 94 | const supported = scriptTable.langSysRecords.map((l) => l.langSysTag); 95 | if (hasDefault) supported.unshift(`dflt`); 96 | return supported; 97 | } 98 | 99 | getDefaultLangSysTable(scriptTable) { 100 | scriptTable = this.ensureScriptTable(scriptTable); 101 | let offset = scriptTable.defaultLangSys; 102 | if (offset !== 0) { 103 | this.parser.currentPosition = scriptTable.start + offset; 104 | let table = new LangSysTable(this.parser); 105 | table.langSysTag = ``; 106 | table.defaultForScript = scriptTable.scriptTag; 107 | return table; 108 | } 109 | } 110 | 111 | getLangSysTable(scriptTable, langSysTag = `dflt`) { 112 | if (langSysTag === `dflt`) return this.getDefaultLangSysTable(scriptTable); 113 | scriptTable = this.ensureScriptTable(scriptTable); 114 | let record = scriptTable.langSysRecords.find( 115 | (l) => l.langSysTag === langSysTag 116 | ); 117 | this.parser.currentPosition = scriptTable.start + record.langSysOffset; 118 | let table = new LangSysTable(this.parser); 119 | table.langSysTag = langSysTag; 120 | return table; 121 | } 122 | 123 | // Feature functions 124 | 125 | getFeatures(langSysTable) { 126 | return langSysTable.featureIndices.map((index) => this.getFeature(index)); 127 | } 128 | 129 | getFeature(indexOrTag) { 130 | let record; 131 | if (parseInt(indexOrTag) == indexOrTag) { 132 | record = this.featureList.featureRecords[indexOrTag]; 133 | } else { 134 | record = this.featureList.featureRecords.find( 135 | (f) => f.featureTag === indexOrTag 136 | ); 137 | } 138 | if (!record) return; 139 | this.parser.currentPosition = this.featureList.start + record.featureOffset; 140 | let table = new FeatureTable(this.parser); 141 | table.featureTag = record.featureTag; 142 | return table; 143 | } 144 | 145 | // Lookup functions 146 | 147 | getLookups(featureTable) { 148 | return featureTable.lookupListIndices.map((index) => this.getLookup(index)); 149 | } 150 | 151 | getLookup(lookupIndex, type) { 152 | let lookupOffset = this.lookupList.lookups[lookupIndex]; 153 | this.parser.currentPosition = this.lookupList.start + lookupOffset; 154 | return new LookupTable(this.parser, type); 155 | } 156 | } 157 | 158 | export { CommonLayoutTable }; 159 | -------------------------------------------------------------------------------- /src/opentype/tables/createTable.js: -------------------------------------------------------------------------------- 1 | // Step 1: set up a namespace for all our table classes. 2 | const tableClasses = {}; 3 | let tableClassesLoaded = false; 4 | 5 | // Step 2: load all the table classes. While the imports 6 | // all resolve asynchronously, Promise.all won't "exit" 7 | // until every class definition has been loaded. 8 | Promise.all([ 9 | // opentype tables 10 | import("./simple/cmap.js"), 11 | import("./simple/head.js"), 12 | import("./simple/hhea.js"), 13 | import("./simple/hmtx.js"), 14 | import("./simple/maxp.js"), 15 | import("./simple/name.js"), 16 | import("./simple/OS2.js"), 17 | import("./simple/post.js"), 18 | 19 | // opentype tables that rely on the "common layout tables" data structures 20 | import("./advanced/BASE.js"), 21 | import("./advanced/GDEF.js"), 22 | import("./advanced/GSUB.js"), 23 | import("./advanced/GPOS.js"), 24 | 25 | // SVG tables... err... table 26 | import("./simple/SVG.js"), 27 | 28 | // Variable fonts 29 | import("./simple/variation/fvar.js"), 30 | 31 | // TTF tables 32 | import("./simple/ttf/cvt.js"), 33 | import("./simple/ttf/fpgm.js"), 34 | import("./simple/ttf/gasp.js"), 35 | import("./simple/ttf/glyf.js"), 36 | import("./simple/ttf/loca.js"), 37 | import("./simple/ttf/prep.js"), 38 | 39 | // CFF 40 | import("./simple/cff/CFF.js"), 41 | import("./simple/cff/CFF2.js"), 42 | import("./simple/cff/VORG.js"), 43 | 44 | // bitmap 45 | import("./simple/bitmap/EBLC.js"), 46 | import("./simple/bitmap/EBDT.js"), 47 | import("./simple/bitmap/EBSC.js"), 48 | import("./simple/bitmap/CBLC.js"), 49 | import("./simple/bitmap/CBDT.js"), 50 | import("./simple/bitmap/sbix.js"), 51 | 52 | // color 53 | import("./simple/color/COLR.js"), 54 | import("./simple/color/CPAL.js"), 55 | 56 | // "other" tables 57 | import("./simple/other/DSIG.js"), 58 | import("./simple/other/hdmx.js"), 59 | import("./simple/other/kern.js"), 60 | import("./simple/other/LTSH.js"), 61 | import("./simple/other/MERG.js"), 62 | import("./simple/other/meta.js"), 63 | import("./simple/other/PCLT.js"), 64 | import("./simple/other/VDMX.js"), 65 | import("./simple/other/vhea.js"), 66 | import("./simple/other/vmtx.js"), 67 | ]) 68 | 69 | // Step 3: rebind all the class imports so that 70 | // we can fetch constructors given table names. 71 | .then((data) => { 72 | data.forEach((e) => { 73 | let name = Object.keys(e)[0]; 74 | tableClasses[name] = e[name]; 75 | }); 76 | tableClassesLoaded = true; 77 | }); 78 | 79 | /** 80 | * Step 4: set up a table factory that can build tables given a name tag. 81 | * @param {*} tables the object containing actual table instances. 82 | * @param {*} dict an object of the form: { tag: "string", offset: , [length: ]} 83 | * @param {*} dataview a DataView object over an ArrayBuffer of Uint8Array 84 | */ 85 | function createTable(tables, dict, dataview) { 86 | let name = dict.tag.replace(/[^\w\d]/g, ``); 87 | let Type = tableClasses[name]; 88 | if (Type) return new Type(dict, dataview, tables); 89 | console.warn( 90 | `lib-font has no definition for ${name}. The table was skipped.` 91 | ); 92 | return {}; 93 | } 94 | 95 | function loadTableClasses() { 96 | let count = 0; 97 | function checkLoaded(resolve, reject) { 98 | if (!tableClassesLoaded) { 99 | if (count > 10) { 100 | return reject(new Error(`loading took too long`)); 101 | } 102 | count++; 103 | return setTimeout(() => checkLoaded(resolve), 250); 104 | } 105 | resolve(createTable); 106 | } 107 | return new Promise((resolve, reject) => checkLoaded(resolve)); 108 | } 109 | 110 | export { loadTableClasses }; 111 | -------------------------------------------------------------------------------- /src/opentype/tables/simple-table.js: -------------------------------------------------------------------------------- 1 | import { Parser, ParsedData } from "../../parser.js"; 2 | 3 | class SimpleTable extends ParsedData { 4 | constructor(dict, dataview, name) { 5 | const { parser, start } = super(new Parser(dict, dataview, name)); 6 | 7 | // alias the parser as "p" 8 | const pGetter = { enumerable: false, get: () => parser }; 9 | Object.defineProperty(this, `p`, pGetter); 10 | 11 | // alias the start offset as "tableStart" 12 | const startGetter = { enumerable: false, get: () => start }; 13 | Object.defineProperty(this, `tableStart`, startGetter); 14 | } 15 | } 16 | 17 | export { SimpleTable }; 18 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/OS2.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `OS/2` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/OS2 7 | */ 8 | class OS2 extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | 12 | this.version = p.uint16; 13 | this.xAvgCharWidth = p.int16; 14 | this.usWeightClass = p.uint16; 15 | this.usWidthClass = p.uint16; 16 | this.fsType = p.uint16; 17 | this.ySubscriptXSize = p.int16; 18 | this.ySubscriptYSize = p.int16; 19 | this.ySubscriptXOffset = p.int16; 20 | this.ySubscriptYOffset = p.int16; 21 | this.ySuperscriptXSize = p.int16; 22 | this.ySuperscriptYSize = p.int16; 23 | this.ySuperscriptXOffset = p.int16; 24 | this.ySuperscriptYOffset = p.int16; 25 | this.yStrikeoutSize = p.int16; 26 | this.yStrikeoutPosition = p.int16; 27 | this.sFamilyClass = p.int16; 28 | this.panose = [...new Array(10)].map((_) => p.uint8); 29 | this.ulUnicodeRange1 = p.flags(32); 30 | this.ulUnicodeRange2 = p.flags(32); 31 | this.ulUnicodeRange3 = p.flags(32); 32 | this.ulUnicodeRange4 = p.flags(32); 33 | this.achVendID = p.tag; 34 | this.fsSelection = p.uint16; 35 | this.usFirstCharIndex = p.uint16; 36 | this.usLastCharIndex = p.uint16; 37 | this.sTypoAscender = p.int16; 38 | this.sTypoDescender = p.int16; 39 | this.sTypoLineGap = p.int16; 40 | this.usWinAscent = p.uint16; 41 | this.usWinDescent = p.uint16; 42 | 43 | if (this.version === 0) return p.verifyLength(); 44 | 45 | this.ulCodePageRange1 = p.flags(32); 46 | this.ulCodePageRange2 = p.flags(32); 47 | 48 | if (this.version === 1) return p.verifyLength(); 49 | 50 | this.sxHeight = p.int16; 51 | this.sCapHeight = p.int16; 52 | this.usDefaultChar = p.uint16; 53 | this.usBreakChar = p.uint16; 54 | this.usMaxContext = p.uint16; 55 | 56 | if (this.version <= 4) return p.verifyLength(); 57 | 58 | this.usLowerOpticalPointSize = p.uint16; 59 | this.usUpperOpticalPointSize = p.uint16; 60 | 61 | if (this.version === 5) return p.verifyLength(); 62 | } 63 | } 64 | 65 | export { OS2 }; 66 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/SVG.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../parser.js"; 2 | import { SimpleTable } from "../simple-table.js"; 3 | 4 | /** 5 | * The OpenType `SVG` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/SVG 8 | */ 9 | class SVG extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | 13 | this.version = p.uint16; 14 | this.offsetToSVGDocumentList = p.Offset32; // from the start of the SVG table 15 | 16 | p.currentPosition = this.tableStart + this.offsetToSVGDocumentList; 17 | this.documentList = new SVGDocumentList(p); 18 | } 19 | } 20 | 21 | /** 22 | * The SVG document list. 23 | */ 24 | class SVGDocumentList extends ParsedData { 25 | constructor(p) { 26 | super(p); 27 | this.numEntries = p.uint16; 28 | this.documentRecords = [...new Array(this.numEntries)].map( 29 | (_) => new SVGDocumentRecord(p) 30 | ); 31 | } 32 | 33 | /** 34 | * Get an SVG document by ID 35 | */ 36 | getDocument(documentID) { 37 | let record = this.documentRecords[documentID]; 38 | if (!record) return ""; 39 | 40 | let offset = this.start + record.svgDocOffset; 41 | this.parser.currentPosition = offset; 42 | return this.parser.readBytes(record.svgDocLength); 43 | } 44 | 45 | /** 46 | * Get an SVG document given a glyphID 47 | */ 48 | getDocumentForGlyph(glyphID) { 49 | let id = this.documentRecords.findIndex( 50 | (d) => d.startGlyphID <= glyphID && glyphID <= d.endGlyphID 51 | ); 52 | if (id === -1) return ""; 53 | return this.getDocument(id); 54 | } 55 | } 56 | 57 | /** 58 | * SVG document record, pointing to a specific SVG document encoding a 59 | * range of glyphs as 19 | [...new Array(this.numSizes)].map((_) => new BitmapSize(p)) 20 | ); 21 | } 22 | } 23 | 24 | export { EBLC }; 25 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/EBSC.js: -------------------------------------------------------------------------------- 1 | import { BitmapScale } from "./shared.js"; 2 | import { SimpleTable } from "../../simple-table.js"; 3 | import lazy from "../../../../lazy.js"; 4 | 5 | /** 6 | * The OpenType `EBSC` table. 7 | * 8 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/EBSC 9 | */ 10 | class EBSC extends SimpleTable { 11 | constructor(dict, dataview) { 12 | const { p } = super(dict, dataview); 13 | 14 | this.majorVersion = p.uint16; 15 | this.minorVersion = p.uint16; 16 | this.numSizes = p.uint32; 17 | 18 | lazy(this, `bitmapScales`, () => 19 | [...new Array(this.numSizes)].map((_) => new BitmapScale(p)) 20 | ); 21 | } 22 | } 23 | 24 | export { EBSC }; 25 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format1.js: -------------------------------------------------------------------------------- 1 | import { SmallGlyphMetrics } from "../shared.js"; 2 | 3 | class Format1 { 4 | constructor(p) { 5 | const mtx = (this.smallMetrics = new SmallGlyphMetrics(p)); 6 | /** 7 | * The bitmap data begins with the most significant bit of the first byte 8 | * corresponding to the top-left pixel of the bounding box, proceeding through 9 | * succeeding bits moving left to right. The data for each row is padded to a 10 | * byte boundary, so the next row begins with the most significant bit of a new byte. 11 | */ 12 | const byteCount = mtx.height * Math.ceil(mtx.width / 8); 13 | this.imageData = p.readBytes(byteCount); 14 | } 15 | } 16 | 17 | export { Format1 }; 18 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format17.js: -------------------------------------------------------------------------------- 1 | import { SmallGlyphMetrics } from "../shared.js"; 2 | 3 | class Format17 { 4 | constructor(p) { 5 | this.glyphMetrics = new SmallGlyphMetrics(p); 6 | this.dataLen = p.uint32; 7 | this.data = p.readBytes(this.dataLen); 8 | } 9 | } 10 | 11 | export { Format17 }; 12 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format18.js: -------------------------------------------------------------------------------- 1 | import { BigGlyphMetrics } from "../shared.js"; 2 | 3 | class Format18 { 4 | constructor(p) { 5 | this.glyphMetrics = new BigGlyphMetrics(p); 6 | this.dataLen = p.uint32; 7 | this.data = p.readBytes(this.dataLen); 8 | } 9 | } 10 | 11 | export { Format18 }; 12 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format19.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Similar to bitmap format 5, the metrics for this format are not 3 | * stored in the bitmap format itself, but in the CBLC table. 4 | */ 5 | class Format19 { 6 | constructor(p) { 7 | this.dataLen = p.uint32; 8 | this.data = p.readBytes(this.dataLen); 9 | } 10 | } 11 | 12 | export { Format19 }; 13 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format2.js: -------------------------------------------------------------------------------- 1 | import { SmallGlyphMetrics } from "../shared.js"; 2 | 3 | class Format2 { 4 | constructor(p) { 5 | const mtx = (this.smallMetrics = new SmallGlyphMetrics(p)); 6 | const byteCount = Math.ceil((mtx.height * mtx.width) / 8); 7 | this.imageData = p.readBytes(byteCount); 8 | } 9 | } 10 | 11 | export { Format2 }; 12 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format3.js: -------------------------------------------------------------------------------- 1 | class Format3 { 2 | constructor(p) { 3 | console.warn( 4 | `As per https://docs.microsoft.com/en-us/typography/opentype/spec/EBDT, bitmap format 3 is obsolete` 5 | ); 6 | } 7 | } 8 | 9 | export { Format3 }; 10 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format4.js: -------------------------------------------------------------------------------- 1 | class Format4 { 2 | constructor(p) { 3 | console.warn( 4 | `As per https://docs.microsoft.com/en-us/typography/opentype/spec/EBDT, bitmap format 4 is not supported by OpenType` 5 | ); 6 | } 7 | } 8 | 9 | export { Format4 }; 10 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Glyph bitmap format 5 is similar to format 2 except that no metrics information is included, 3 | * just the bit aligned data. This format is for use with EBLC indexSubTable format 2 or format 4 | * 5, which will contain the metrics information for all glyphs. 5 | */ 6 | class Format5 { 7 | constructor(p, h, w) { 8 | this.imageData = p.readBytes(Math.ceil((h * w) / 8)); 9 | } 10 | } 11 | 12 | export { Format5 }; 13 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format6.js: -------------------------------------------------------------------------------- 1 | import { BigGlyphMetrics } from "../shared.js"; 2 | 3 | /** 4 | * format 1 for big metrics 5 | */ 6 | class Format6 { 7 | constructor(p) { 8 | const mtx = (this.smallMetrics = new BigGlyphMetrics(p)); 9 | /** 10 | * The bitmap data begins with the most significant bit of the first byte 11 | * corresponding to the top-left pixel of the bounding box, proceeding through 12 | * succeeding bits moving left to right. The data for each row is padded to a 13 | * byte boundary, so the next row begins with the most significant bit of a new byte. 14 | */ 15 | const byteCount = mtx.height * Math.ceil(mtx.width / 8); 16 | this.imageData = p.readBytes(byteCount); 17 | } 18 | } 19 | 20 | export { Format6 }; 21 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format7.js: -------------------------------------------------------------------------------- 1 | import { BigGlyphMetrics } from "../shared.js"; 2 | 3 | /** 4 | * format 2 for big metrics 5 | */ 6 | class Format7 { 7 | constructor(p) { 8 | const mtx = (this.smallMetrics = new BigGlyphMetrics(p)); 9 | const byteCount = Math.ceil((mtx.height * mtx.width) / 8); 10 | this.imageData = p.readBytes(byteCount); 11 | } 12 | } 13 | 14 | export { Format7 }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format8.js: -------------------------------------------------------------------------------- 1 | import { SmallGlyphMetrics, EBDTComponent } from "../shared.js"; 2 | 3 | class Format8 { 4 | constructor(p) { 5 | this.smallMetrics = SmallGlyphMetrics; 6 | p.uint8; // padding byte, makes the next fields align with format9 7 | this.numComponents = p.uint16; 8 | this.components = [...new Array(this.numComponents)].map( 9 | (_) => new EBDTComponent(p) 10 | ); 11 | } 12 | } 13 | 14 | export { Format8 }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/formats/format9.js: -------------------------------------------------------------------------------- 1 | import { BigGlyphMetrics, EBDTComponent } from "../shared.js"; 2 | 3 | class Format9 { 4 | constructor(p) { 5 | this.smallMetrics = BigGlyphMetrics; 6 | // no padding byte, already aligned with format8 7 | this.numComponents = p.uint16; 8 | this.components = [...new Array(this.numComponents)].map( 9 | (_) => new EBDTComponent(p) 10 | ); 11 | } 12 | } 13 | 14 | export { Format9 }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/sbix.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../parser.js"; 2 | import { SimpleTable } from "../../simple-table.js"; 3 | import lazy from "../../../../lazy.js"; 4 | 5 | /** 6 | * The OpenType `sbix` table. 7 | * 8 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/sbix 9 | * 10 | * Notes: 11 | * The glyph count is derived from the 'maxp' table. Advance and side-bearing 12 | * glyph metrics are stored in the 'hmtx' table for horizontal layout, and 13 | * the 'vmtx' table for vertical layout. 14 | * 15 | */ 16 | class sbix extends SimpleTable { 17 | constructor(dict, dataview) { 18 | const { p } = super(dict, dataview); 19 | 20 | this.version = p.uint16; 21 | this.flags = p.flags(16); 22 | this.numStrikes = p.uint32; 23 | lazy(this, `strikeOffsets`, () => 24 | [...new Array(this.numStrikes)].map((_) => p.Offset32) 25 | ); // from the beginning of the 'sbix' table 26 | } 27 | 28 | // TODO: add a strike accessor 29 | } 30 | 31 | class Strike extends ParsedData { 32 | constructor(p, numGlyphs) { 33 | this.ppem = p.uint16; 34 | this.ppi = p.uint16; 35 | lazy(this, `glyphDataOffsets`, () => 36 | [...new Array(numGlyphs + 1)].map((_) => p.Offset32) 37 | ); // from the beginning of the strike data header 38 | } 39 | 40 | // TODO: add a glyph data accessor 41 | } 42 | 43 | class GlyphData { 44 | constructor(p) { 45 | this.originOffsetX = p.int16; 46 | this.originOffsetY = p.int16; 47 | this.graphicType = p.tag; 48 | 49 | // The actual embedded graphic data has a byte length that is inferred from sequential 50 | // entries in the strike.glyphDataOffsets array + the fixed size (8 bytes) of the preceding fields. 51 | const len = 0; 52 | lazy(this, `data`, () => p.readBytes(len)); 53 | 54 | // TODO: make this.data load in the correct data 55 | } 56 | } 57 | 58 | export { sbix }; 59 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/shared.js: -------------------------------------------------------------------------------- 1 | class BitmapSize { 2 | constructor(p) { 3 | this.indexSubTableArrayOffset = p.Offset32; // from beginning of CBLC 4 | this.indexTablesSize = p.uint32; 5 | this.numberofIndexSubTables = p.uint32; 6 | this.colorRef = p.uint32; 7 | this.hori = new SbitLineMetrics(p); 8 | this.vert = new SbitLineMetrics(p); 9 | this.startGlyphIndex = p.uint16; 10 | this.endGlyphIndex = p.uint16; 11 | this.ppemX = p.uint8; 12 | this.ppemY = p.uint8; 13 | this.bitDepth = p.uint8; 14 | this.flags = p.int8; 15 | } 16 | } 17 | 18 | class BitmapScale { 19 | constructor(p) { 20 | this.hori = new SbitLineMetrics(p); 21 | this.vert = new SbitLineMetrics(p); 22 | this.ppemX = p.uint8; 23 | this.ppemY = p.uint8; 24 | this.substitutePpemX = p.uint8; 25 | this.substitutePpemY = p.uint8; 26 | } 27 | } 28 | 29 | class SbitLineMetrics { 30 | constructor(p) { 31 | this.ascender = p.int8; 32 | this.descender = p.int8; 33 | this.widthMax = p.uint8; 34 | this.caretSlopeNumerator = p.int8; 35 | this.caretSlopeDenominator = p.int8; 36 | this.caretOffset = p.int8; 37 | this.minOriginSB = p.int8; 38 | this.minAdvanceSB = p.int8; 39 | this.maxBeforeBL = p.int8; 40 | this.minAfterBL = p.int8; 41 | this.pad1 = p.int8; 42 | this.pad2 = p.int8; 43 | } 44 | } 45 | 46 | class IndexSubHeader { 47 | constructor(p) { 48 | this.indexFormat = p.uint16; 49 | this.imageFormat = p.uint16; 50 | this.imageDataOffset = p.Offset32; // Offset to image data in EBDT table. 51 | } 52 | } 53 | 54 | class BigGlyphMetrics { 55 | constructor(p) { 56 | this.height = uint8; 57 | this.width = uint8; 58 | this.horiBearingX = int8; 59 | this.horiBearingY = int8; 60 | this.horiAdvance = uint8; 61 | this.vertBearingX = int8; 62 | this.vertBearingY = int8; 63 | this.vertAdvance = uint8; 64 | } 65 | } 66 | 67 | class SmallGlyphMetrics { 68 | constructor(p) { 69 | this.height = p.uint8; 70 | this.width = p.uint8; 71 | this.bearingX = p.int8; 72 | this.bearingY = p.int8; 73 | this.advance = p.uint8; 74 | } 75 | } 76 | 77 | class EBDTComponent { 78 | constructor(p) { 79 | this.glyphID = p.uint16; 80 | this.xOffset = p.int8; 81 | this.yOffset = p.int8; 82 | } 83 | } 84 | 85 | class IndexSubTableArray { 86 | constructor(p) { 87 | this.firstGlyphIndex = p.uint16; 88 | this.lastGlyphIndex = p.uint16; 89 | this.additionalOffsetToIndexSubtable = p.Offset32; // from beginning of EBLC. 90 | } 91 | } 92 | 93 | export { 94 | BitmapSize, 95 | BitmapScale, 96 | SbitLineMetrics, 97 | IndexSubHeader, 98 | BigGlyphMetrics, 99 | SmallGlyphMetrics, 100 | EBDTComponent, 101 | IndexSubTableArray, 102 | }; 103 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/subtables/subtable1.js: -------------------------------------------------------------------------------- 1 | import { IndexSubHeader } from "../shared.js"; 2 | 3 | /** 4 | * IndexSubTable1: variable-metrics glyphs with 4-byte offsets 5 | */ 6 | class Subtable1 { 7 | constructor(p) { 8 | super(p); 9 | this.header = new IndexSubHeader(p); 10 | 11 | /* 12 | The documentation says: 13 | 14 | offsetArray[glyphIndex] + imageDataOffset = glyphData sizeOfArray = (lastGlyph - firstGlyph + 1) + 1 + 1 pad if needed 15 | 16 | To which I say: "... what? O_o" 17 | 18 | TODO: figure out how to size this array 19 | */ 20 | const len = 0; 21 | this.offsetArray = [...new Array(len)].map((_) => p.Offset32); 22 | } 23 | } 24 | 25 | export { Subtable1 }; 26 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/subtables/subtable2.js: -------------------------------------------------------------------------------- 1 | import { IndexSubHeader, BigGlyphMetrics } from "../shared.js"; 2 | 3 | /** 4 | * IndexSubTable2: all glyphs have identical metrics 5 | */ 6 | class Subtable2 { 7 | constructor(p) { 8 | super(p); 9 | this.header = new IndexSubHeader(p); 10 | this.imageSize = p.uint32; 11 | this.bigMetrics = new BigGlyphMetrics(p); 12 | } 13 | } 14 | 15 | export { Subtable2 }; 16 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/subtables/subtable3.js: -------------------------------------------------------------------------------- 1 | import { IndexSubHeader } from "../shared.js"; 2 | 3 | /** 4 | * IndexSubTable3: variable-metrics glyphs with 2-byte offsets 5 | */ 6 | class Subtable3 { 7 | constructor(p) { 8 | super(p); 9 | this.header = new IndexSubHeader(p); 10 | 11 | /* 12 | The documentation says: 13 | 14 | offsetArray[glyphIndex] + imageDataOffset = glyphData sizeOfArray = (lastGlyph - firstGlyph + 1) + 1 + 1 pad if needed 15 | 16 | To which I again say: "... what? O_o" 17 | 18 | TODO: figure out how to size this array 19 | */ 20 | const len = 0; 21 | this.offsetArray = [...new Array(len)].map((_) => p.Offset16); 22 | } 23 | } 24 | 25 | export { Subtable3 }; 26 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/subtables/subtable4.js: -------------------------------------------------------------------------------- 1 | import { IndexSubHeader } from "../shared.js"; 2 | import lazy from "../../../../../lazy.js"; 3 | 4 | /** 5 | * IndexSubTable4: variable-metrics glyphs with sparse glyph codes 6 | */ 7 | class Subtable4 { 8 | constructor(p) { 9 | super(p); 10 | 11 | this.header = new IndexSubHeader(p); 12 | this.numGlyphs = p.uint32; 13 | 14 | lazy(this, `glyphArray`, () => 15 | [...new Array(this.numGlyphs + 1)].map((_) => new GlyphIdOffsetPair(p)) 16 | ); 17 | } 18 | } 19 | 20 | class GlyphIdOffsetPair { 21 | constructor(p) { 22 | this.glyphID = p.uint16; 23 | this.offset = p.Offset16; // Location in EBDT (TODO: figure out what "in" means) 24 | } 25 | } 26 | 27 | export { Subtable4 }; 28 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/bitmap/subtables/subtable5.js: -------------------------------------------------------------------------------- 1 | import { IndexSubHeader, BigGlyphMetrics } from "../shared.js"; 2 | import lazy from "../../../../../lazy.js"; 3 | 4 | /** 5 | * IndexSubTable5: constant-metrics glyphs with sparse glyph codes 6 | */ 7 | class Subtable5 { 8 | constructor(p) { 9 | super(p); 10 | 11 | this.header = new IndexSubHeader(p); 12 | this.imageSize = p.uint32; 13 | this.bigMetrics = new BigGlyphMetrics(p); 14 | this.numGlyphs = p.uint32; 15 | 16 | lazy(this, `glyphIdArray`, () => 17 | [...new Array(this.numGlyphs)].map((_) => p.uint16) 18 | ); 19 | } 20 | } 21 | 22 | export { Subtable5 }; 23 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cff/CFF.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `CFF` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/CFF 8 | */ 9 | class CFF extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | lazy(this, `data`, () => p.readBytes()); 13 | } 14 | } 15 | 16 | export { CFF }; 17 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cff/CFF2.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `CFF2` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/CFF2 8 | */ 9 | class CFF2 extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | lazy(this, `data`, () => p.readBytes()); 13 | } 14 | } 15 | 16 | export { CFF2 }; 17 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cff/VORG.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `VORG` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/VORG 8 | */ 9 | class VORG extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | 13 | this.majorVersion = p.uint16; 14 | this.minorVersion = p.uint16; 15 | this.defaultVertOriginY = p.int16; 16 | this.numVertOriginYMetrics = p.uint16; 17 | 18 | lazy(this, `vertORiginYMetrics`, () => 19 | [...new Array(this.numVertOriginYMetrics)].map( 20 | (_) => new VertOriginYMetric(p) 21 | ) 22 | ); 23 | } 24 | } 25 | 26 | class VertOriginYMetric { 27 | constructor(p) { 28 | this.glyphIndex = p.uint16; 29 | this.vertOriginY = p.int16; 30 | } 31 | } 32 | 33 | export { VORG }; 34 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../simple-table.js"; 2 | import createSubTable from "./cmap/createSubTable.js"; 3 | import lazy from "../../../lazy.js"; 4 | 5 | /** 6 | * The OpenType `cmap` main table. 7 | * 8 | * Subtables are found in the ./cmap directory 9 | * 10 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/cmap for more information 11 | */ 12 | class cmap extends SimpleTable { 13 | constructor(dict, dataview) { 14 | const { p } = super(dict, dataview); 15 | this.version = p.uint16; 16 | this.numTables = p.uint16; 17 | this.encodingRecords = [...new Array(this.numTables)].map( 18 | (_) => new EncodingRecord(p, this.tableStart) 19 | ); 20 | } 21 | 22 | getSubTable(tableID) { 23 | return this.encodingRecords[tableID].table; 24 | } 25 | 26 | getSupportedEncodings() { 27 | return this.encodingRecords.map((r) => ({ 28 | platformID: r.platformID, 29 | encodingID: r.encodingID, 30 | })); 31 | } 32 | 33 | getSupportedCharCodes(platformID, encodingID) { 34 | const recordID = this.encodingRecords.findIndex( 35 | (r) => r.platformID === platformID && r.encodingID === encodingID 36 | ); 37 | if (recordID === -1) return false; 38 | const subtable = this.getSubTable(recordID); 39 | return subtable.getSupportedCharCodes(); 40 | } 41 | 42 | reverse(glyphid) { 43 | for (let i = 0; i < this.numTables; i++) { 44 | let code = this.getSubTable(i).reverse(glyphid); 45 | if (code) return code; 46 | } 47 | } 48 | 49 | getGlyphId(char) { 50 | let last = 0; 51 | this.encodingRecords.some((_, tableID) => { 52 | let t = this.getSubTable(tableID); 53 | if (!t.getGlyphId) return false; 54 | last = t.getGlyphId(char); 55 | return last !== 0; 56 | }); 57 | return last; 58 | } 59 | 60 | supports(char) { 61 | return this.encodingRecords.some((_, tableID) => { 62 | const t = this.getSubTable(tableID); 63 | return t.supports && t.supports(char) !== false; 64 | }); 65 | } 66 | 67 | supportsVariation(variation) { 68 | return this.encodingRecords.some((_, tableID) => { 69 | const t = this.getSubTable(tableID); 70 | return t.supportsVariation && t.supportsVariation(variation) !== false; 71 | }); 72 | } 73 | } 74 | 75 | /** 76 | * ...docs go here... 77 | */ 78 | class EncodingRecord { 79 | constructor(p, tableStart) { 80 | const platformID = (this.platformID = p.uint16); 81 | const encodingID = (this.encodingID = p.uint16); 82 | const offset = (this.offset = p.Offset32); // from cmap table start 83 | 84 | lazy(this, `table`, () => { 85 | p.currentPosition = tableStart + offset; 86 | return createSubTable(p, platformID, encodingID); 87 | }); 88 | } 89 | } 90 | 91 | export { cmap }; 92 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/README.md: -------------------------------------------------------------------------------- 1 | See https://docs.microsoft.com/en-us/typography/opentype/spec/cmap 2 | 3 | all submaps are enriched with a `.platformID` and `.encodingID` property so that they can perform correct reverse lookups for glyph IDs. 4 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/createSubTable.js: -------------------------------------------------------------------------------- 1 | // cmap subtables 2 | 3 | import { Format0 } from "./format0.js"; 4 | import { Format2 } from "./format2.js"; 5 | import { Format4 } from "./format4.js"; 6 | import { Format6 } from "./format6.js"; 7 | import { Format8 } from "./format8.js"; 8 | import { Format10 } from "./format10.js"; 9 | import { Format12 } from "./format12.js"; 10 | import { Format13 } from "./format13.js"; 11 | import { Format14 } from "./format14.js"; 12 | 13 | /** 14 | * Cmap Subtable factory 15 | * @param {int} format the subtable format number (see https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#format-0-byte-encoding-table onward) 16 | * @param {parser} parser a parser already pointing at the subtable's data location, right after reading the `format` uint16. 17 | */ 18 | export default function createSubTable(parser, platformID, encodingID) { 19 | const format = parser.uint16; 20 | if (format === 0) return new Format0(parser, platformID, encodingID); 21 | if (format === 2) return new Format2(parser, platformID, encodingID); 22 | if (format === 4) return new Format4(parser, platformID, encodingID); 23 | if (format === 6) return new Format6(parser, platformID, encodingID); 24 | if (format === 8) return new Format8(parser, platformID, encodingID); 25 | if (format === 10) return new Format10(parser, platformID, encodingID); 26 | if (format === 12) return new Format12(parser, platformID, encodingID); 27 | if (format === 13) return new Format13(parser, platformID, encodingID); 28 | if (format === 14) return new Format14(parser, platformID, encodingID); 29 | return {}; 30 | } 31 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format0.js: -------------------------------------------------------------------------------- 1 | import { Subtable } from "./subtable.js"; 2 | 3 | class Format0 extends Subtable { 4 | constructor(p, platformID, encodingID) { 5 | super(p, platformID, encodingID); 6 | this.format = 0; 7 | this.length = p.uint16; 8 | this.language = p.uint16; 9 | // this isn't worth lazy-loading 10 | this.glyphIdArray = [...new Array(256)].map((_) => p.uint8); 11 | } 12 | 13 | supports(charCode) { 14 | if (charCode.charCodeAt) { 15 | // TODO: FIXME: map this character to a number based on the Apple standard character to glyph mapping 16 | charCode = -1; 17 | console.warn( 18 | `supports(character) not implemented for cmap subtable format 0. only supports(id) is implemented.` 19 | ); 20 | } 21 | return 0 <= charCode && charCode <= 255; 22 | } 23 | 24 | reverse(glyphID) { 25 | console.warn(`reverse not implemented for cmap subtable format 0`); 26 | return {}; 27 | } 28 | 29 | getSupportedCharCodes() { 30 | return [{ start: 1, end: 256 }]; 31 | } 32 | } 33 | 34 | export { Format0 }; 35 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format10.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | // basically Format 6, but for 32 bit characters 5 | class Format10 extends Subtable { 6 | constructor(p, platformID, encodingID) { 7 | super(p, platformID, encodingID); 8 | this.format = 10; 9 | p.uint16; 10 | this.length = p.uint32; 11 | this.language = p.uint32; 12 | this.startCharCode = p.uint32; 13 | this.numChars = p.uint32; 14 | this.endCharCode = this.startCharCode + this.numChars; 15 | const getter = () => [...new Array(this.numChars)].map((_) => p.uint16); 16 | lazy(this, `glyphs`, getter); 17 | } 18 | 19 | supports(charCode) { 20 | if (charCode.charCodeAt) { 21 | // TODO: FIXME: This can be anything, and depends on the Macintosh language indicated by this.language... 22 | charCode = -1; 23 | console.warn( 24 | `supports(character) not implemented for cmap subtable format 10. only supports(id) is implemented.` 25 | ); 26 | } 27 | if (charCode < this.startCharCode) return false; 28 | if (charCode > this.startCharCode + this.numChars) return false; 29 | return charCode - this.startCharCode; 30 | } 31 | 32 | reverse(glyphID) { 33 | console.warn(`reverse not implemented for cmap subtable format 10`); 34 | return {}; 35 | } 36 | 37 | getSupportedCharCodes(preservePropNames = false) { 38 | if (preservePropNames) { 39 | return [ 40 | { startCharCode: this.startCharCode, endCharCode: this.endCharCode }, 41 | ]; 42 | } 43 | return [{ start: this.startCharCode, end: this.endCharCode }]; 44 | } 45 | } 46 | 47 | export { Format10 }; 48 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format12.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | // basically Format 8, but for 32 bit characters 5 | class Format12 extends Subtable { 6 | constructor(p, platformID, encodingID) { 7 | super(p, platformID, encodingID); 8 | this.format = 12; 9 | p.uint16; 10 | this.length = p.uint32; 11 | this.language = p.uint32; 12 | this.numGroups = p.uint32; 13 | const getter = () => 14 | [...new Array(this.numGroups)].map((_) => new SequentialMapGroup(p)); 15 | lazy(this, `groups`, getter); 16 | } 17 | 18 | supports(charCode) { 19 | if (charCode.charCodeAt) charCode = charCode.charCodeAt(0); 20 | 21 | // surrogate pair value? 22 | if (0xd800 <= charCode && charCode <= 0xdfff) return 0; 23 | 24 | // one of the exactly 66 noncharacters? 25 | if ((charCode & 0xfffe) === 0xfffe || (charCode & 0xffff) === 0xffff) 26 | return 0; 27 | 28 | return ( 29 | this.groups.findIndex( 30 | (s) => s.startCharCode <= charCode && charCode <= s.endCharCode 31 | ) !== -1 32 | ); 33 | } 34 | 35 | reverse(glyphID) { 36 | for (let group of this.groups) { 37 | let start = group.startGlyphID; 38 | if (start > glyphID) continue; 39 | if (start === glyphID) return group.startCharCode; 40 | let end = start + (group.endCharCode - group.startCharCode); 41 | if (end < glyphID) continue; 42 | const code = group.startCharCode + (glyphID - start); 43 | return { code, unicode: String.fromCodePoint(code) }; 44 | } 45 | return {}; 46 | } 47 | 48 | getSupportedCharCodes(preservePropNames = false) { 49 | if (preservePropNames) return this.groups; 50 | return this.groups.map((v) => ({ 51 | start: v.startCharCode, 52 | end: v.endCharCode, 53 | })); 54 | } 55 | } 56 | 57 | class SequentialMapGroup { 58 | constructor(p) { 59 | this.startCharCode = p.uint32; 60 | this.endCharCode = p.uint32; 61 | this.startGlyphID = p.uint32; 62 | } 63 | } 64 | 65 | export { Format12 }; 66 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format13.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | class Format13 extends Subtable { 5 | constructor(p, platformID, encodingID) { 6 | super(p, platformID, encodingID); 7 | this.format = 13; 8 | p.uint16; 9 | this.length = p.uint32; 10 | this.language = p.uint32; 11 | this.numGroups = p.uint32; 12 | const getter = [...new Array(this.numGroups)].map( 13 | (_) => new ConstantMapGroup(p) 14 | ); 15 | lazy(this, `groups`, getter); 16 | } 17 | 18 | supports(charCode) { 19 | if (charCode.charCodeAt) charCode = charCode.charCodeAt(0); // assumed safe, might not be? 20 | return ( 21 | this.groups.findIndex( 22 | (s) => s.startCharCode <= charCode && charCode <= s.endCharCode 23 | ) !== -1 24 | ); 25 | } 26 | 27 | reverse(glyphID) { 28 | console.warn(`reverse not implemented for cmap subtable format 13`); 29 | return {}; 30 | } 31 | 32 | getSupportedCharCodes(preservePropNames = false) { 33 | if (preservePropNames) return this.groups; 34 | return this.groups.map((v) => ({ 35 | start: v.startCharCode, 36 | end: v.endCharCode, 37 | })); 38 | } 39 | } 40 | 41 | class ConstantMapGroup { 42 | constructor(p) { 43 | this.startCharCode = p.uint32; 44 | this.endCharCode = p.uint32; 45 | this.glyphID = p.uint32; 46 | } 47 | } 48 | 49 | export { Format13 }; 50 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format14.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | class Format14 extends Subtable { 5 | constructor(p, platformID, encodingID) { 6 | super(p, platformID, encodingID); 7 | this.subTableStart = p.currentPosition; 8 | this.format = 14; 9 | this.length = p.uint32; 10 | this.numVarSelectorRecords = p.uint32; 11 | lazy(this, `varSelectors`, () => 12 | [...new Array(this.numVarSelectorRecords)].map( 13 | (_) => new VariationSelector(p) 14 | ) 15 | ); 16 | } 17 | 18 | supports() { 19 | console.warn(`supports not implemented for cmap subtable format 14`); 20 | return 0; 21 | } 22 | 23 | getSupportedCharCodes() { 24 | console.warn( 25 | `getSupportedCharCodes not implemented for cmap subtable format 14` 26 | ); 27 | return []; 28 | } 29 | 30 | reverse(glyphID) { 31 | console.warn(`reverse not implemented for cmap subtable format 14`); 32 | return {}; 33 | } 34 | 35 | supportsVariation(variation) { 36 | let v = this.varSelector.find((uvs) => uvs.varSelector === variation); 37 | return v ? v : false; 38 | } 39 | 40 | getSupportedVariations() { 41 | return this.varSelectors.map((v) => v.varSelector); 42 | } 43 | } 44 | 45 | class VariationSelector { 46 | constructor(p) { 47 | this.varSelector = p.uint24; 48 | this.defaultUVSOffset = p.Offset32; 49 | this.nonDefaultUVSOffset = p.Offset32; 50 | } 51 | } 52 | 53 | export { Format14 }; 54 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format2.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | class Format2 extends Subtable { 5 | constructor(p, platformID, encodingID) { 6 | super(p, platformID, encodingID); 7 | this.format = 2; 8 | this.length = p.uint16; 9 | this.language = p.uint16; 10 | this.subHeaderKeys = [...new Array(256)].map((_) => p.uint16); 11 | 12 | const subHeaderCount = Math.max(...this.subHeaderKeys); 13 | 14 | const subHeaderOffset = p.currentPosition; 15 | lazy(this, `subHeaders`, () => { 16 | p.currentPosition = subHeaderOffset; 17 | return [...new Array(subHeaderCount)].map((_) => new SubHeader(p)); 18 | }); 19 | 20 | const glyphIndexOffset = subHeaderOffset + subHeaderCount * 8; 21 | lazy(this, `glyphIndexArray`, () => { 22 | p.currentPosition = glyphIndexOffset; 23 | return [...new Array(subHeaderCount)].map((_) => p.uint16); 24 | }); 25 | } 26 | 27 | supports(charCode) { 28 | if (charCode.charCodeAt) { 29 | // TODO: FIXME: consider implementing the correct mapping, https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#format-2-high-byte-mapping-through-table 30 | charCode = -1; 31 | console.warn( 32 | `supports(character) not implemented for cmap subtable format 2. only supports(id) is implemented.` 33 | ); 34 | } 35 | 36 | const low = charCode && 0xff; 37 | const high = charCode && 0xff00; 38 | const subHeaderKey = this.subHeaders[high]; 39 | const subheader = this.subHeaders[subHeaderKey]; 40 | const first = subheader.firstCode; 41 | const last = first + subheader.entryCount; 42 | return first <= low && low <= last; 43 | } 44 | 45 | reverse(glyphID) { 46 | console.warn(`reverse not implemented for cmap subtable format 2`); 47 | return {}; 48 | } 49 | 50 | getSupportedCharCodes(preservePropNames = false) { 51 | if (preservePropNames) { 52 | return this.subHeaders.map((h) => ({ 53 | firstCode: h.firstCode, 54 | lastCode: h.lastCode, 55 | })); 56 | } 57 | return this.subHeaders.map((h) => ({ 58 | start: h.firstCode, 59 | end: h.lastCode, 60 | })); 61 | } 62 | } 63 | 64 | class SubHeader { 65 | constructor(p) { 66 | this.firstCode = p.uint16; 67 | this.entryCount = p.uint16; 68 | this.lastCode = this.first + this.entryCount; 69 | this.idDelta = p.int16; 70 | this.idRangeOffset = p.uint16; 71 | } 72 | } 73 | 74 | export { Format2 }; 75 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format4.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | class Format4 extends Subtable { 5 | constructor(p, platformID, encodingID) { 6 | super(p, platformID, encodingID); 7 | this.format = 4; 8 | this.length = p.uint16; 9 | this.language = p.uint16; 10 | this.segCountX2 = p.uint16; 11 | this.segCount = this.segCountX2 / 2; 12 | this.searchRange = p.uint16; 13 | this.entrySelector = p.uint16; 14 | this.rangeShift = p.uint16; 15 | 16 | // This cmap subformat basically lazy-loads everything. It would be better to 17 | // not even lazy load but the code is not ready for selective extraction. 18 | 19 | const endCodePosition = p.currentPosition; 20 | lazy(this, `endCode`, () => 21 | p.readBytes(this.segCount, endCodePosition, 16) 22 | ); 23 | 24 | const startCodePosition = endCodePosition + 2 + this.segCountX2; 25 | lazy(this, `startCode`, () => 26 | p.readBytes(this.segCount, startCodePosition, 16) 27 | ); 28 | 29 | const idDeltaPosition = startCodePosition + this.segCountX2; 30 | lazy( 31 | this, 32 | `idDelta`, 33 | () => p.readBytes(this.segCount, idDeltaPosition, 16, true) // Note that idDelta values are signed 34 | ); 35 | 36 | const idRangePosition = idDeltaPosition + this.segCountX2; 37 | lazy(this, `idRangeOffset`, () => 38 | p.readBytes(this.segCount, idRangePosition, 16) 39 | ); 40 | 41 | const glyphIdArrayPosition = idRangePosition + this.segCountX2; 42 | const glyphIdArrayLength = 43 | this.length - (glyphIdArrayPosition - this.tableStart); 44 | lazy(this, `glyphIdArray`, () => 45 | p.readBytes(glyphIdArrayLength, glyphIdArrayPosition, 16) 46 | ); 47 | 48 | // also, while not in the spec, we really want to organise all that data into convenient segments 49 | lazy(this, `segments`, () => 50 | this.buildSegments(idRangePosition, glyphIdArrayPosition, p) 51 | ); 52 | } 53 | 54 | buildSegments(idRangePosition, glyphIdArrayPosition, p) { 55 | const build = (_, i) => { 56 | let startCode = this.startCode[i], 57 | endCode = this.endCode[i], 58 | idDelta = this.idDelta[i], 59 | idRangeOffset = this.idRangeOffset[i], 60 | idRangeOffsetPointer = idRangePosition + 2 * i, 61 | glyphIDs = []; 62 | 63 | // simple case 64 | if (idRangeOffset === 0) { 65 | for (let i = startCode + idDelta, e = endCode + idDelta; i <= e; i++) { 66 | glyphIDs.push(i); 67 | } 68 | } 69 | 70 | // not so simple case 71 | else { 72 | for (let i = 0, e = endCode - startCode; i <= e; i++) { 73 | p.currentPosition = idRangeOffsetPointer + idRangeOffset + i * 2; 74 | glyphIDs.push(p.uint16); 75 | } 76 | } 77 | 78 | return { startCode, endCode, idDelta, idRangeOffset, glyphIDs }; 79 | }; 80 | 81 | return [...new Array(this.segCount)].map(build); 82 | } 83 | 84 | reverse(glyphID) { 85 | let s = this.segments.find((v) => v.glyphIDs.includes(glyphID)); 86 | if (!s) return {}; 87 | const code = s.startCode + s.glyphIDs.indexOf(glyphID); 88 | return { code, unicode: String.fromCodePoint(code) }; 89 | } 90 | 91 | getGlyphId(charCode) { 92 | if (charCode.charCodeAt) charCode = charCode.charCodeAt(0); 93 | 94 | // surrogate pair value? 95 | if (0xd800 <= charCode && charCode <= 0xdfff) return 0; 96 | 97 | // one of the exactly 66 noncharacters? 98 | if ((charCode & 0xfffe) === 0xfffe || (charCode & 0xffff) === 0xffff) 99 | return 0; 100 | 101 | let segment = this.segments.find( 102 | (s) => s.startCode <= charCode && charCode <= s.endCode 103 | ); 104 | if (!segment) return 0; 105 | return segment.glyphIDs[charCode - segment.startCode]; 106 | } 107 | 108 | supports(charCode) { 109 | return this.getGlyphId(charCode) !== 0; 110 | } 111 | 112 | getSupportedCharCodes(preservePropNames = false) { 113 | if (preservePropNames) return this.segments; 114 | return this.segments.map((v) => ({ start: v.startCode, end: v.endCode })); 115 | } 116 | } 117 | 118 | export { Format4 }; 119 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format6.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | class Format6 extends Subtable { 5 | constructor(p, platformID, encodingID) { 6 | super(p, platformID, encodingID); 7 | this.format = 6; 8 | this.length = p.uint16; 9 | this.language = p.uint16; 10 | this.firstCode = p.uint16; 11 | this.entryCount = p.uint16; 12 | this.lastCode = this.firstCode + this.entryCount - 1; 13 | 14 | const getter = () => [...new Array(this.entryCount)].map((_) => p.uint16); 15 | lazy(this, `glyphIdArray`, getter); 16 | } 17 | 18 | supports(charCode) { 19 | if (charCode.charCodeAt) { 20 | // TODO: FIXME: This can be anything, and depends on the Macintosh language indicated by this.language... 21 | charCode = -1; 22 | console.warn( 23 | `supports(character) not implemented for cmap subtable format 6. only supports(id) is implemented.` 24 | ); 25 | } 26 | if (charCode < this.firstCode) return {}; 27 | if (charCode > this.firstCode + this.entryCount) return {}; 28 | const code = charCode - this.firstCode; 29 | return { code, unicode: String.fromCodePoint(code) }; 30 | } 31 | 32 | reverse(glyphID) { 33 | let pos = this.glyphIdArray.indexOf(glyphID); 34 | if (pos > -1) return this.firstCode + pos; 35 | } 36 | 37 | getSupportedCharCodes(preservePropNames = false) { 38 | if (preservePropNames) { 39 | return [{ firstCode: this.firstCode, lastCode: this.lastCode }]; 40 | } 41 | return [{ start: this.firstCode, end: this.lastCode }]; 42 | } 43 | } 44 | 45 | export { Format6 }; 46 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/format8.js: -------------------------------------------------------------------------------- 1 | import lazy from "../../../../lazy.js"; 2 | import { Subtable } from "./subtable.js"; 3 | 4 | class Format8 extends Subtable { 5 | constructor(p, platformID, encodingID) { 6 | super(p, platformID, encodingID); 7 | this.format = 8; 8 | p.uint16; 9 | this.length = p.uint32; 10 | this.language = p.uint32; 11 | this.is32 = [...new Array(8192)].map((_) => p.uint8); 12 | this.numGroups = p.uint32; 13 | const getter = () => 14 | [...new Array(this.numGroups)].map((_) => new SequentialMapGroup(p)); 15 | lazy(this, `groups`, getter); 16 | } 17 | 18 | supports(charCode) { 19 | if (charCode.charCodeAt) { 20 | // TODO: FIXME: https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#format-8-mixed-16-bit-and-32-bit-coverage is kind of incredible 21 | charCode = -1; 22 | console.warn( 23 | `supports(character) not implemented for cmap subtable format 8. only supports(id) is implemented.` 24 | ); 25 | } 26 | return ( 27 | this.groups.findIndex( 28 | (s) => s.startcharCode <= charCode && charCode <= s.endcharCode 29 | ) !== -1 30 | ); 31 | } 32 | 33 | reverse(glyphID) { 34 | console.warn(`reverse not implemented for cmap subtable format 8`); 35 | return {}; 36 | } 37 | 38 | getSupportedCharCodes(preservePropNames = false) { 39 | if (preservePropNames) return this.groups; 40 | return this.groups.map((v) => ({ 41 | start: v.startcharCode, 42 | end: v.endcharCode, 43 | })); 44 | } 45 | } 46 | 47 | class SequentialMapGroup { 48 | constructor(p) { 49 | this.startcharCode = p.uint32; 50 | this.endcharCode = p.uint32; 51 | this.startGlyphID = p.uint32; 52 | } 53 | } 54 | 55 | export { Format8 }; 56 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/cmap/subtable.js: -------------------------------------------------------------------------------- 1 | import { ParsedData } from "../../../../parser.js"; 2 | 3 | class Subtable extends ParsedData { 4 | constructor(p, plaformID, encodingID) { 5 | super(p); 6 | this.plaformID = plaformID; 7 | this.encodingID = encodingID; 8 | } 9 | } 10 | 11 | export { Subtable }; 12 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/color/COLR.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `COLR` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/COLR 7 | */ 8 | class COLR extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | this.version = p.uint16; 12 | this.numBaseGlyphRecords = p.uint16; 13 | this.baseGlyphRecordsOffset = p.Offset32; // from beginning of COLR table) to Base Glyph records. 14 | this.layerRecordsOffset = p.Offset32; // from beginning of COLR table) to Layer Records. 15 | this.numLayerRecords = p.uint16; 16 | } 17 | 18 | getBaseGlyphRecord(glyphID) { 19 | // the documentation recommends doing a binary search to find the record, 20 | // and so we shall. The size of a BaseGlyphRecord is 6 bytes, so off we go! 21 | let start = this.tableStart + this.baseGlyphRecordsOffset; 22 | this.parser.currentPosition = start; 23 | let first = new BaseGlyphRecord(this.parser); 24 | let firstID = first.gID; 25 | 26 | let end = this.tableStart + this.layerRecordsOffset - 6; 27 | this.parser.currentPosition = end; 28 | let last = new BaseGlyphRecord(this.parser); 29 | let lastID = last.gID; 30 | 31 | // right. Onward, to victory! 32 | if (firstID === glyphID) return first; 33 | if (lastID === glyphID) return last; 34 | 35 | // delayed gratification! 36 | while (true) { 37 | if (start === end) break; 38 | let mid = start + (end - start) / 12; 39 | this.parser.currentPosition = mid; 40 | let middle = new BaseGlyphRecord(this.parser); 41 | let midID = middle.gID; 42 | 43 | if (midID === glyphID) return middle; 44 | // curses! 45 | else if (midID > glyphID) { 46 | end = mid; 47 | } else if (midID < glyphID) { 48 | start = mid; 49 | } 50 | } 51 | 52 | return false; 53 | } 54 | 55 | getLayers(glyphID) { 56 | let record = this.getBaseGlyphRecord(glyphID); 57 | this.parser.currentPosition = 58 | this.tableStart + this.layerRecordsOffset + 4 * record.firstLayerIndex; 59 | return [...new Array(record.numLayers)].map((_) => new LayerRecord(p)); 60 | } 61 | } 62 | 63 | class BaseGlyphRecord { 64 | constructor(p) { 65 | this.gID = p.uint16; 66 | this.firstLayerIndex = p.uint16; 67 | this.numLayers = p.uint16; 68 | } 69 | } 70 | 71 | class LayerRecord { 72 | constructor(p) { 73 | this.gID = p.uint16; 74 | this.paletteIndex = p.uint16; 75 | } 76 | } 77 | 78 | export { COLR }; 79 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/color/CPAL.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `CPAL` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/CPAL 8 | */ 9 | class CPAL extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | 13 | this.version = p.uint16; 14 | this.numPaletteEntries = p.uint16; 15 | const numPalettes = (this.numPalettes = p.uint16); 16 | this.numColorRecords = p.uint16; 17 | this.offsetFirstColorRecord = p.Offset32; 18 | this.colorRecordIndices = [...new Array(this.numPalettes)].map( 19 | (_) => p.uint16 20 | ); 21 | 22 | lazy(this, `colorRecords`, () => { 23 | p.currentPosition = this.tableStart + this.offsetFirstColorRecord; 24 | return [...new Array(this.numColorRecords)].map( 25 | (_) => new ColorRecord(p) 26 | ); 27 | }); 28 | 29 | // Index of each palette’s first color record in the combined color record array. 30 | 31 | if (this.version === 1) { 32 | this.offsetPaletteTypeArray = p.Offset32; // from the beginning of CPAL table to the Palette Type Array. 33 | this.offsetPaletteLabelArray = p.Offset32; // from the beginning of CPAL table to the Palette Labels Array. 34 | this.offsetPaletteEntryLabelArray = p.Offset32; // from the beginning of CPAL table to the Palette Entry Label Array. 35 | 36 | lazy(this, `paletteTypeArray`, () => { 37 | p.currentPosition = this.tableStart + this.offsetPaletteTypeArray; 38 | return new PaletteTypeArray(p, numPalettes); 39 | }); 40 | 41 | lazy(this, `paletteLabelArray`, () => { 42 | p.currentPosition = this.tableStart + this.offsetPaletteLabelArray; 43 | return new PaletteLabelsArray(p, numPalettes); 44 | }); 45 | 46 | lazy(this, `paletteEntryLabelArray`, () => { 47 | p.currentPosition = this.tableStart + this.offsetPaletteEntryLabelArray; 48 | return new PaletteEntryLabelArray(p, numPalettes); 49 | }); 50 | } 51 | } 52 | } 53 | 54 | class ColorRecord { 55 | constructor(p) { 56 | this.blue = p.uint8; 57 | this.green = p.uint8; 58 | this.red = p.uint8; 59 | this.alpha = p.uint8; 60 | } 61 | } 62 | 63 | class PaletteTypeArray { 64 | constructor(p, numPalettes) { 65 | // see https://docs.microsoft.com/en-us/typography/opentype/spec/cpal#palette-type-array 66 | this.paletteTypes = [...new Array(numPalettes)].map((_) => p.uint32); 67 | } 68 | } 69 | 70 | class PaletteLabelsArray { 71 | constructor(p, numPalettes) { 72 | // see https://docs.microsoft.com/en-us/typography/opentype/spec/cpal#palette-labels-array 73 | this.paletteLabels = [...new Array(numPalettes)].map((_) => p.uint16); 74 | } 75 | } 76 | 77 | class PaletteEntryLabelArray { 78 | constructor(p, numPalettes) { 79 | // see https://docs.microsoft.com/en-us/typography/opentype/spec/cpal#palette-entry-label-array 80 | this.paletteEntryLabels = [...new Array(numPalettes)].map((_) => p.uint16); 81 | } 82 | } 83 | 84 | export { CPAL }; 85 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/head.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `head` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/head 7 | */ 8 | class head extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | this.load({ 12 | majorVersion: p.uint16, 13 | minorVersion: p.uint16, 14 | fontRevision: p.fixed, 15 | checkSumAdjustment: p.uint32, 16 | magicNumber: p.uint32, 17 | flags: p.flags(16), 18 | unitsPerEm: p.uint16, 19 | created: p.longdatetime, 20 | modified: p.longdatetime, 21 | xMin: p.int16, 22 | yMin: p.int16, 23 | xMax: p.int16, 24 | yMax: p.int16, 25 | macStyle: p.flags(16), 26 | lowestRecPPEM: p.uint16, 27 | fontDirectionHint: p.uint16, 28 | indexToLocFormat: p.uint16, 29 | glyphDataFormat: p.uint16, 30 | }); 31 | } 32 | } 33 | 34 | export { head }; 35 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/hhea.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `hhea` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/hhea 7 | */ 8 | class hhea extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | 12 | this.majorVersion = p.uint16; 13 | this.minorVersion = p.uint16; 14 | this.ascender = p.fword; 15 | this.descender = p.fword; 16 | this.lineGap = p.fword; 17 | this.advanceWidthMax = p.ufword; 18 | this.minLeftSideBearing = p.fword; 19 | this.minRightSideBearing = p.fword; 20 | this.xMaxExtent = p.fword; 21 | this.caretSlopeRise = p.int16; 22 | this.caretSlopeRun = p.int16; 23 | this.caretOffset = p.int16; 24 | p.int16; 25 | p.int16; 26 | p.int16; 27 | p.int16; 28 | this.metricDataFormat = p.int16; 29 | this.numberOfHMetrics = p.uint16; 30 | 31 | p.verifyLength(); 32 | } 33 | } 34 | 35 | export { hhea }; 36 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/hmtx.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../simple-table.js"; 2 | import lazy from "../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `hmtx` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/hmtx 8 | */ 9 | class hmtx extends SimpleTable { 10 | constructor(dict, dataview, tables) { 11 | const { p } = super(dict, dataview); 12 | 13 | const numberOfHMetrics = tables.hhea.numberOfHMetrics; 14 | const numGlyphs = tables.maxp.numGlyphs; 15 | 16 | const metricsStart = p.currentPosition; 17 | lazy(this, `hMetrics`, () => { 18 | p.currentPosition = metricsStart; 19 | return [...new Array(numberOfHMetrics)].map( 20 | (_) => new LongHorMetric(p.uint16, p.int16) 21 | ); 22 | }); 23 | 24 | if (numberOfHMetrics < numGlyphs) { 25 | const lsbStart = metricsStart + numberOfHMetrics * 4; 26 | lazy(this, `leftSideBearings`, () => { 27 | p.currentPosition = lsbStart; 28 | return [...new Array(numGlyphs - numberOfHMetrics)].map((_) => p.int16); 29 | }); 30 | } 31 | } 32 | } 33 | 34 | class LongHorMetric { 35 | constructor(w, b) { 36 | this.advanceWidth = w; 37 | this.lsb = b; 38 | } 39 | } 40 | 41 | export { hmtx }; 42 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/maxp.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `maxp` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/maxp 7 | */ 8 | class maxp extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | 12 | this.version = p.legacyFixed; 13 | this.numGlyphs = p.uint16; 14 | 15 | if (this.version === 1) { 16 | this.maxPoints = p.uint16; 17 | this.maxContours = p.uint16; 18 | this.maxCompositePoints = p.uint16; 19 | this.maxCompositeContours = p.uint16; 20 | this.maxZones = p.uint16; 21 | this.maxTwilightPoints = p.uint16; 22 | this.maxStorage = p.uint16; 23 | this.maxFunctionDefs = p.uint16; 24 | this.maxInstructionDefs = p.uint16; 25 | this.maxStackElements = p.uint16; 26 | this.maxSizeOfInstructions = p.uint16; 27 | this.maxComponentElements = p.uint16; 28 | this.maxComponentDepth = p.uint16; 29 | } 30 | 31 | p.verifyLength(); 32 | } 33 | } 34 | 35 | export { maxp }; 36 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/name.js: -------------------------------------------------------------------------------- 1 | import { Parser } from "../../../parser.js"; 2 | import { SimpleTable } from "../simple-table.js"; 3 | import lazy from "../../../lazy.js"; 4 | 5 | /** 6 | * The OpenType `name` table. 7 | * 8 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/name 9 | */ 10 | class name extends SimpleTable { 11 | constructor(dict, dataview) { 12 | const { p } = super(dict, dataview); 13 | 14 | this.format = p.uint16; 15 | this.count = p.uint16; 16 | this.stringOffset = p.Offset16; // relative to start of table 17 | 18 | // name records 19 | this.nameRecords = [...new Array(this.count)].map( 20 | (_) => new NameRecord(p, this) 21 | ); 22 | 23 | // lang-tag records, if applicable 24 | if (this.format === 1) { 25 | this.langTagCount = p.uint16; 26 | this.langTagRecords = [...new Array(this.langTagCount)].map( 27 | (_) => new LangTagRecord(p.uint16, p.Offset16) 28 | ); 29 | } 30 | 31 | // cache these values for use in `.get(nameID)` 32 | this.stringStart = this.tableStart + this.stringOffset; 33 | } 34 | 35 | /** 36 | * Resolve a string by ID 37 | * @param {uint16} nameID the id used to find the name record to resolve. 38 | */ 39 | get(nameID) { 40 | let record = this.nameRecords.find((record) => record.nameID === nameID); 41 | if (record) return record.string; 42 | } 43 | } 44 | 45 | /** 46 | * ...docs go here... 47 | */ 48 | class LangTagRecord { 49 | constructor(length, offset) { 50 | this.length = length; 51 | this.offset = offset; 52 | } 53 | } 54 | 55 | /** 56 | * ...docs go here... 57 | */ 58 | class NameRecord { 59 | constructor(p, nameTable) { 60 | this.platformID = p.uint16; 61 | this.encodingID = p.uint16; 62 | this.languageID = p.uint16; 63 | this.nameID = p.uint16; 64 | this.length = p.uint16; 65 | this.offset = p.Offset16; 66 | 67 | lazy(this, `string`, () => { 68 | p.currentPosition = nameTable.stringStart + this.offset; 69 | return decodeString(p, this); 70 | }); 71 | } 72 | } 73 | 74 | /** 75 | * Specific platforms and platform/encoding combinations encode strings in 76 | * different ways. 77 | */ 78 | function decodeString(p, record) { 79 | const { platformID, length } = record; 80 | 81 | if (length === 0) return ``; 82 | 83 | // We decode strings for the Unicode/Microsoft platforms as UTF-16 84 | if (platformID === 0 || platformID === 3) { 85 | const str = []; 86 | for (let i = 0, e = length / 2; i < e; i++) 87 | str[i] = String.fromCharCode(p.uint16); 88 | return str.join(``); 89 | } 90 | 91 | // Everything else, we treat as plain bytes. 92 | const bytes = p.readBytes(length); 93 | const str = []; 94 | bytes.forEach(function (b, i) { 95 | str[i] = String.fromCharCode(b); 96 | }); 97 | return str.join(``); 98 | 99 | // TODO: if someone wants to finesse this/implement all the other string encodings, have at it! 100 | } 101 | 102 | export { name }; 103 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/DSIG.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `DSIG` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/DSIG 7 | */ 8 | class DSIG extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | this.version = p.uint32; 12 | this.numSignatures = p.uint16; 13 | this.flags = p.uint16; 14 | this.signatureRecords = [...new Array(this.numSignatures)].map( 15 | (_) => new SignatureRecord(p) 16 | ); 17 | } 18 | 19 | getData(signatureID) { 20 | const record = this.signatureRecords[signatureID]; 21 | this.parser.currentPosition = this.tableStart + record.offset; 22 | return new SignatureBlockFormat1(this.parser); 23 | } 24 | } 25 | 26 | class SignatureRecord { 27 | constructor(p) { 28 | this.format = p.uint32; 29 | this.length = p.uint32; 30 | this.offset = p.Offset32; // from the beginning of the DSIG table 31 | } 32 | } 33 | 34 | class SignatureBlockFormat1 { 35 | // "Signature blocks may have various formats; currently one format is defined." 36 | // There is so much optimism here. There might be more formats! We should reserve 37 | // _multiple_ uint16! We have BIG PLANS! ...some time before 2002! 38 | constructor(p) { 39 | p.uint16; 40 | p.uint16; 41 | this.signatureLength = p.uint32; 42 | this.signature = p.readBytes(this.signatureLength); // this is a PKCS#7 packet, and you get to deadl with that yourself. 43 | } 44 | } 45 | 46 | export { DSIG }; 47 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/LTSH.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `LTSH` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/LTSH 7 | */ 8 | class LTSH extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | this.version = p.uint16; 12 | this.numGlyphs = p.uint16; 13 | this.yPels = p.readBytes(this.numGlyphs); 14 | } 15 | } 16 | 17 | export { LTSH }; 18 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/MERG.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `MERG` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/MERG 8 | */ 9 | class MERG extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | this.version = p.uint16; 13 | this.mergeClassCount = p.uint16; 14 | this.mergeDataOffset = p.Offset16; // from... where? 15 | this.classDefCount = p.uint16; 16 | this.offsetToClassDefOffsets = p.Offset16; // from the start of the MERG table. 17 | 18 | // This is a big 2D array 19 | lazy(this, `mergeEntryMatrix`, () => 20 | [...new Array(this.mergeClassCount)].map((_) => 21 | p.readBytes(this.mergeClassCount) 22 | ) 23 | ); 24 | 25 | console.warn(`Full MERG parsing is currently not supported.`); 26 | console.warn( 27 | `If you need this table parsed, please file an issue, or better yet, a PR.` 28 | ); 29 | } 30 | } 31 | 32 | export { MERG }; 33 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/PCLT.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `PCLT` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/PCLT 7 | */ 8 | class PCLT extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | console.warn( 12 | `This font uses a PCLT table, which is currently not supported by this parser.` 13 | ); 14 | console.warn( 15 | `If you need this table parsed, please file an issue, or better yet, a PR.` 16 | ); 17 | } 18 | } 19 | 20 | export { PCLT }; 21 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/VDMX.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `VDMX` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/VDMX 7 | */ 8 | class VDMX extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | this.version = p.uint16; 12 | this.numRecs = p.uint16; 13 | this.numRatios = p.uint16; 14 | this.ratRanges = [...new Array(this.numRatios)].map( 15 | (_) => new RatioRange(p) 16 | ); 17 | this.offsets = [...new Array(this.numRatios)].map((_) => p.Offset16); // start of this table to the VDMXGroup table for a corresponding RatioRange record. 18 | this.VDMXGroups = [...new Array(this.numRecs)].map((_) => new VDMXGroup(p)); 19 | } 20 | } 21 | 22 | class RatioRange { 23 | constructor(p) { 24 | this.bCharSet = p.uint8; 25 | this.xRatio = p.uint8; 26 | this.yStartRatio = p.uint8; 27 | this.yEndRatio = p.uint8; 28 | } 29 | } 30 | 31 | class VDMXGroup { 32 | constructor(p) { 33 | this.recs = p.uint16; 34 | this.startsz = p.uint8; 35 | this.endsz = p.uint8; 36 | this.records = [...new Array(this.recs)].map((_) => new vTable(p)); 37 | } 38 | } 39 | 40 | class vTable { 41 | constructor(p) { 42 | this.yPelHeight = p.uint16; 43 | this.yMax = p.int16; 44 | this.yMin = p.int16; 45 | } 46 | } 47 | 48 | export { VDMX }; 49 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/hdmx.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `hdmx` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/hdmx 7 | */ 8 | class hdmx extends SimpleTable { 9 | constructor(dict, dataview, tables) { 10 | const { p } = super(dict, dataview); 11 | const numGlyphs = tables.hmtx.numGlyphs; 12 | this.version = p.uint16; 13 | this.numRecords = p.int16; 14 | this.sizeDeviceRecord = p.int32; 15 | this.records = [...new Array(numRecords)].map( 16 | (_) => new DeviceRecord(p, numGlyphs) 17 | ); 18 | } 19 | } 20 | 21 | class DeviceRecord { 22 | constructor(p, numGlyphs) { 23 | this.pixelSize = p.uint8; 24 | this.maxWidth = p.uint8; 25 | this.widths = p.readBytes(numGlyphs); 26 | } 27 | } 28 | 29 | export { hdmx }; 30 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/kern.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `kern` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/kern 8 | * 9 | * Also don't use this table anymore =( 10 | */ 11 | class kern extends SimpleTable { 12 | constructor(dict, dataview) { 13 | const { p } = super(dict, dataview); 14 | this.version = p.uint16; 15 | this.nTables = p.uint16; 16 | 17 | // getting this data is hilarious, because I'm intentionally not implementing subtable 2 18 | lazy(this, `tables`, () => { 19 | let offset = this.tableStart + 4; 20 | const tables = []; 21 | for (let i = 0; i < this.nTables; i++) { 22 | p.currentPosition = offset; 23 | let subtable = new KernSubTable(p); 24 | tables.push(subtable); 25 | offset += subtable; 26 | } 27 | return tables; 28 | }); 29 | } 30 | } 31 | 32 | class KernSubTable { 33 | constructor(p) { 34 | this.version = p.uint16; 35 | this.length = p.uint16; // length of subtable (including the header) 36 | 37 | // We deviate from the spec here, because it's ridiculous. 38 | // The spec says we have a uin16 that represents 16 bits. 39 | // Then you read the description of how to treat those bits, 40 | // and you realise it's NOT 16 bits, it's 8 bits of which 41 | // bits 0-3 are used, and bits 4-7 are reserved, and then it's 42 | // a plain uint8 "format" value. So that's what we do here. 43 | this.coverage = p.flags(8); 44 | this.format = p.uint8; 45 | 46 | if (this.format === 0) { 47 | this.nPairs = p.uint16; 48 | this.searchRange = p.uint16; 49 | this.entrySelector = p.uint16; 50 | this.rangeShift = p.uint16; 51 | lazy(this, `pairs`, () => 52 | [...new Array(this.nPairs)].map((_) => new Pair(p)) 53 | ); 54 | } 55 | 56 | if (this.format === 2) { 57 | // Wow. Not only does this font have a kern table, it has a kern table that isn't universally supported. Classy. 58 | console.warn( 59 | `Kern subtable format 2 is not supported: this parser currently only parses universal table data.` 60 | ); 61 | } 62 | } 63 | 64 | get horizontal() { 65 | return this.coverage[0]; 66 | } 67 | get minimum() { 68 | return this.coverage[1]; 69 | } 70 | get crossstream() { 71 | return this.coverage[2]; 72 | } 73 | get override() { 74 | return this.coverage[3]; 75 | } 76 | } 77 | 78 | class Pair { 79 | constructor(p) { 80 | this.left = p.uint16; 81 | this.right = p.uint16; 82 | this.value = p.fword; 83 | } 84 | } 85 | 86 | export { kern }; 87 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/meta.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `meta` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/meta 7 | */ 8 | class meta extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | 12 | this.version = p.uint32; 13 | this.flags = p.uint32; 14 | p.uint32; 15 | this.dataMapsCount = p.uint32; 16 | 17 | this.dataMaps = [...new Array(this.dataMapsCount)].map( 18 | (_) => new DataMap(this.tableStart, p) 19 | ); 20 | } 21 | } 22 | 23 | class DataMap { 24 | constructor(tableStart, p) { 25 | this.tableStart = tableStart; 26 | this.parser = p; 27 | 28 | this.tag = p.tag; 29 | this.dataOffset = p.Offset32; // from the beginning of the metadata table to the data for this tag. 30 | this.dataLength = p.uint32; 31 | } 32 | 33 | getData() { 34 | // If you need this data, you're on the hook for parsing it properly. 35 | this.parser.currentField = this.tableStart + this.dataOffset; 36 | return this.parser.readBytes(this.dataLength); 37 | } 38 | } 39 | 40 | export { meta }; 41 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/vhea.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `vhea` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/vhea 7 | */ 8 | class vhea extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | 12 | this.version = p.fixed; 13 | // the next three fields are named as per version 1.0 and version 1.1 14 | this.ascent = this.vertTypoAscender = p.int16; 15 | this.descent = this.vertTypoDescender = p.int16; 16 | this.lineGap = this.vertTypoLineGap = p.int16; 17 | this.advanceHeightMax = p.int16; 18 | this.minTopSideBearing = p.int16; 19 | this.minBottomSideBearing = p.int16; 20 | this.yMaxExtent = p.int16; 21 | this.caretSlopeRise = p.int16; 22 | this.caretSlopeRun = p.int16; 23 | this.caretOffset = p.int16; 24 | this.reserved = p.int16; 25 | this.reserved = p.int16; 26 | this.reserved = p.int16; 27 | this.reserved = p.int16; 28 | this.metricDataFormat = p.int16; 29 | this.numOfLongVerMetrics = p.uint16; 30 | 31 | p.verifyLength(); 32 | } 33 | } 34 | 35 | export { vhea }; 36 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/other/vmtx.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `vmtx` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/vmtx 8 | * 9 | * The overall structure of the vertical metrics table consists of two arrays: 10 | * a vMetrics array, followed by an array of top side bearings. 11 | * 12 | */ 13 | class vmtx extends SimpleTable { 14 | constructor(dict, dataview, tables) { 15 | const { p } = super(dict, dataview); 16 | const numOfLongVerMetrics = tables.vhea.numOfLongVerMetrics; 17 | const numGlyphs = tables.maxp.numGlyphs; 18 | 19 | const metricsStart = p.currentPosition; 20 | lazy(this, `vMetrics`, () => { 21 | p.currentPosition = metricsStart; 22 | return [...new Array(numOfLongVerMetrics)].map( 23 | (_) => new LongVerMetric(p.uint16, p.int16) 24 | ); 25 | }); 26 | 27 | if (numOfLongVerMetrics < numGlyphs) { 28 | const tsbStart = metricsStart + numOfLongVerMetrics * 4; 29 | lazy(this, `topSideBearings`, () => { 30 | p.currentPosition = tsbStart; 31 | return [...new Array(numGlyphs - numOfLongVerMetrics)].map( 32 | (_) => p.int16 33 | ); 34 | }); 35 | } 36 | } 37 | } 38 | 39 | class LongVerMetric { 40 | // https://learn.microsoft.com/en-us/typography/opentype/spec/vmtx#vertical-metrics-table-format 41 | constructor(h, b) { 42 | this.advanceHeight = h; 43 | this.topSideBearing = b; 44 | } 45 | } 46 | 47 | export { vmtx }; 48 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/ttf/cvt.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `cvt` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/cvt 8 | */ 9 | class cvt extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | // 13 | // The actual data is n instructions, where n is the number of 14 | // FWORD items that fit in the size of the table. That is: 15 | // 16 | // n = table length / sizeof(int16) 17 | // = table length / 2; 18 | // 19 | const n = dict.length / 2; 20 | lazy(this, `items`, () => [...new Array(n)].map((_) => p.fword)); 21 | } 22 | } 23 | 24 | export { cvt }; 25 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/ttf/fpgm.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `fpgm` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/fpgm 8 | */ 9 | class fpgm extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | // 13 | // The actual data is n instructions, where n is the number of 14 | // uint8 items that fit in the size of the table... so, table.length 15 | // 16 | lazy(this, `instructions`, () => 17 | [...new Array(dict.length)].map((_) => p.uint8) 18 | ); 19 | } 20 | } 21 | 22 | export { fpgm }; 23 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/ttf/gasp.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `gasp` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/gasp 8 | */ 9 | class gasp extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | 13 | this.version = p.uint16; 14 | this.numRanges = p.uint16; 15 | 16 | const getter = () => 17 | [...new Array(this.numRanges)].map((_) => new GASPRange(p)); 18 | lazy(this, `gaspRanges`, getter); 19 | } 20 | } 21 | 22 | /** 23 | * GASPRange record 24 | */ 25 | class GASPRange { 26 | constructor(p) { 27 | this.rangeMaxPPEM = p.uint16; 28 | this.rangeGaspBehavior = p.uint16; 29 | } 30 | } 31 | 32 | export { gasp }; 33 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/ttf/glyf.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `glyf` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/glyf 7 | */ 8 | class glyf extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | // This table is not really a table, but a pure data block. 12 | } 13 | 14 | getGlyphData(offset, length) { 15 | this.parser.currentPosition = this.tableStart + offset; 16 | return this.parser.readBytes(length); 17 | } 18 | } 19 | 20 | export { glyf }; 21 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/ttf/loca.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `loca` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/loca 8 | */ 9 | class loca extends SimpleTable { 10 | constructor(dict, dataview, tables) { 11 | const { p } = super(dict, dataview); 12 | 13 | const n = tables.maxp.numGlyphs + 1; // "plus one" because the offset list needs one extra element to determine the block length for the last supported glyph. 14 | 15 | if (tables.head.indexToLocFormat === 0) { 16 | this.x2 = true; 17 | lazy(this, `offsets`, () => [...new Array(n)].map((_) => p.Offset16)); 18 | } else { 19 | lazy(this, `offsets`, () => [...new Array(n)].map((_) => p.Offset32)); 20 | } 21 | } 22 | 23 | getGlyphDataOffsetAndLength(glyphID) { 24 | let offset = this.offsets[glyphID] * this.x2 ? 2 : 1; 25 | let nextOffset = this.offsets[glyphID + 1] * this.x2 ? 2 : 1; 26 | return { offset, length: nextOffset - offset }; 27 | } 28 | } 29 | 30 | export { loca }; 31 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/ttf/prep.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `prep` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/prep 8 | */ 9 | class prep extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | // 13 | // The actual data is n instructions, where n is the number of 14 | // uint8 items that fit in the size of the table... so, table.length 15 | // 16 | lazy(this, `instructions`, () => 17 | [...new Array(dict.length)].map((_) => p.uint8) 18 | ); 19 | } 20 | } 21 | 22 | export { prep }; 23 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/HVAR.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `HVAR` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/HVAR 7 | */ 8 | class HVAR extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | } 12 | } 13 | 14 | export { HVAR }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/MVAR.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `MVAR` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/MVAR 7 | */ 8 | class MVAR extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | } 12 | } 13 | 14 | export { MVAR }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/STAT.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `STAT` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/STAT 7 | */ 8 | class STAT extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | } 12 | } 13 | 14 | export { STAT }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/VVAR.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `VVAR` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/VVAR 7 | */ 8 | class VVAR extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | } 12 | } 13 | 14 | export { VVAR }; 15 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/avar.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `avar` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/avar 7 | */ 8 | class avar extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | this.majorVersion = p.uint16; 12 | this.minorVersion = p.uint16; 13 | p.uint16; 14 | this.axisCount = p.uint16; 15 | this.axisSegmentMaps = [...new Array(this.axisCount)].map( 16 | (_) => new SegmentMap(p) 17 | ); 18 | } 19 | } 20 | 21 | class SegmentMap { 22 | constructor(p) { 23 | this.positionMapCount = p.uint16; 24 | this.axisValueMaps = [...new Array(this.positionMapCount)].map( 25 | (_) => new AxisValueMap(p) 26 | ); 27 | } 28 | } 29 | 30 | class AxisValueMap { 31 | constructor(p) { 32 | this.fromCoordinate = p.F2DOT14; 33 | this.toCoordinate = p.F2DOT14; 34 | } 35 | } 36 | 37 | export { avar }; 38 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/cvar.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `cvar` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/cvar 8 | */ 9 | class cvar extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | this.majorVersion = uint16; 13 | this.minorVersion = uint16; 14 | this.tupleVariationCount = uint16; 15 | this.dataOffset = p.Offset32; // from the start of the table 16 | 17 | // FIXME: this is only correct if we can properly read full Tuple Variation Header... 18 | lazy(this`tupleVariationHeaders`, () => 19 | [...new Array(this.tupleVariationCount)].map( 20 | (_) => new TupleVariationHeader(p) 21 | ) 22 | ); 23 | } 24 | } 25 | 26 | /** 27 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/otvarcommonformats 28 | */ 29 | class TupleVariationHeader { 30 | constructor(p) { 31 | this.variationDataSize = p.uint16; 32 | 33 | // tupleIndex is a packed field: 34 | // - the high 4 bits are flags 35 | // - the low 12 bits are an index into a shared tuple records array. 36 | this.tupleIndex = p.uint16; 37 | 38 | // FIXME: not all tuples are actually there, it depends on the flags. 39 | 40 | this.peakTuple = new Tuple(p); 41 | this.intermediateStartTuple = new Tuple(p); 42 | this.intermediateEndTuple = new Tuple(p); 43 | } 44 | 45 | get EMBEDDED_PEAK_TUPLE() { 46 | return this.tupleIndex & (0x8000 === 0x8000); 47 | } 48 | get INTERMEDIATE_REGION() { 49 | return this.tupleIndex & (0x4000 === 0x4000); 50 | } 51 | get PRIVATE_POINT_NUMBERS() { 52 | return this.tupleIndex & (0x2000 === 0x2000); 53 | } 54 | get realTupleIndex() { 55 | return this.tupleIndex & 0xfff; 56 | } 57 | } 58 | 59 | class Tuple { 60 | constructor(p) { 61 | // FIXME: what... goes in here? 62 | } 63 | } 64 | 65 | export { cvar }; 66 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/fvar.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | import lazy from "../../../../lazy.js"; 3 | 4 | /** 5 | * The OpenType `fvar` table. 6 | * 7 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/fvar 8 | */ 9 | class fvar extends SimpleTable { 10 | constructor(dict, dataview) { 11 | const { p } = super(dict, dataview); 12 | 13 | this.majorVersion = p.uint16; 14 | this.minorVersion = p.uint16; 15 | this.axesArrayOffset = p.Offset16; 16 | p.uint16; 17 | this.axisCount = p.uint16; 18 | this.axisSize = p.uint16; 19 | this.instanceCount = p.uint16; 20 | this.instanceSize = p.uint16; 21 | 22 | const axisStart = this.tableStart + this.axesArrayOffset; 23 | lazy(this, `axes`, () => { 24 | p.currentPosition = axisStart; 25 | return [...new Array(this.axisCount)].map( 26 | (_) => new VariationAxisRecord(p) 27 | ); 28 | }); 29 | 30 | const instanceStart = axisStart + this.axisCount * this.axisSize; 31 | lazy(this, `instances`, () => { 32 | let instances = []; 33 | for (let i = 0; i < this.instanceCount; i++) { 34 | p.currentPosition = instanceStart + i * this.instanceSize; 35 | instances.push( 36 | new InstanceRecord(p, this.axisCount, this.instanceSize) 37 | ); 38 | } 39 | return instances; 40 | }); 41 | } 42 | 43 | getSupportedAxes() { 44 | return this.axes.map((a) => a.tag); 45 | } 46 | 47 | getAxis(name) { 48 | return this.axes.find((a) => a.tag === name); 49 | } 50 | } 51 | 52 | class VariationAxisRecord { 53 | constructor(p) { 54 | this.tag = p.tag; 55 | this.minValue = p.fixed; 56 | this.defaultValue = p.fixed; 57 | this.maxValue = p.fixed; 58 | this.flags = p.flags(16); 59 | this.axisNameID = p.uint16; 60 | } 61 | } 62 | 63 | class InstanceRecord { 64 | constructor(p, axisCount, size) { 65 | let start = p.currentPosition; 66 | this.subfamilyNameID = p.uint16; 67 | p.uint16; 68 | this.coordinates = [...new Array(axisCount)].map((_) => p.fixed); 69 | if (p.currentPosition - start < size) { 70 | this.postScriptNameID = p.uint16; 71 | } 72 | } 73 | } 74 | 75 | export { fvar }; 76 | -------------------------------------------------------------------------------- /src/opentype/tables/simple/variation/gvar.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "../../simple-table.js"; 2 | 3 | /** 4 | * The OpenType `gvar` table. 5 | * 6 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/gvar 7 | */ 8 | class gvar extends SimpleTable { 9 | constructor(dict, dataview) { 10 | const { p } = super(dict, dataview); 11 | } 12 | } 13 | 14 | export { gvar }; 15 | -------------------------------------------------------------------------------- /src/opentype/woff.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "./tables/simple-table.js"; 2 | import lazy from "../lazy.js"; 3 | 4 | const gzipDecode = globalThis.pako ? globalThis.pako.inflate : undefined; 5 | let nativeGzipDecode = undefined; 6 | 7 | if (!gzipDecode) { 8 | import("zlib").then((zlib) => { 9 | nativeGzipDecode = (buffer) => zlib.unzipSync(buffer); 10 | }); 11 | } 12 | 13 | /** 14 | * The WOFF header 15 | * 16 | * See https://www.w3.org/TR/WOFF for WOFF information 17 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/overview for font information 18 | */ 19 | class WOFF extends SimpleTable { 20 | constructor(font, dataview, createTable) { 21 | const { p } = super({ offset: 0, length: 44 }, dataview, `woff`); 22 | 23 | this.signature = p.tag; 24 | this.flavor = p.uint32; 25 | this.length = p.uint32; 26 | this.numTables = p.uint16; 27 | p.uint16; 28 | this.totalSfntSize = p.uint32; 29 | this.majorVersion = p.uint16; 30 | this.minorVersion = p.uint16; 31 | this.metaOffset = p.uint32; 32 | this.metaLength = p.uint32; 33 | this.metaOrigLength = p.uint32; 34 | this.privOffset = p.uint32; 35 | this.privLength = p.uint32; 36 | 37 | p.verifyLength(); 38 | 39 | this.directory = [...new Array(this.numTables)].map( 40 | (_) => new WoffTableDirectoryEntry(p) 41 | ); 42 | buildWoffLazyLookups(this, dataview, createTable); 43 | } 44 | } 45 | 46 | /** 47 | * ... 48 | */ 49 | class WoffTableDirectoryEntry { 50 | constructor(p) { 51 | this.tag = p.tag; 52 | this.offset = p.uint32; 53 | this.compLength = p.uint32; 54 | this.origLength = p.uint32; 55 | this.origChecksum = p.uint32; 56 | } 57 | } 58 | 59 | /** 60 | * Build late-evaluating properties for each table in a 61 | * woff/woff2 font, so that accessing a table via the 62 | * woff.tables.tableName or woff2.tables.tableName 63 | * property kicks off a table parse on first access. 64 | * 65 | * @param {*} woff the woff or woff2 font object 66 | * @param {DataView} dataview passed when dealing with woff 67 | * @param {buffer} decoded passed when dealing with woff2 68 | */ 69 | function buildWoffLazyLookups(woff, dataview, createTable) { 70 | woff.tables = {}; 71 | woff.directory.forEach((entry) => { 72 | lazy(woff.tables, entry.tag.trim(), () => { 73 | let offset = 0; 74 | let view = dataview; 75 | // compressed data? 76 | if (entry.compLength !== entry.origLength) { 77 | const data = dataview.buffer.slice( 78 | entry.offset, 79 | entry.offset + entry.compLength 80 | ); 81 | 82 | let unpacked; 83 | if (gzipDecode) { 84 | unpacked = gzipDecode(new Uint8Array(data)); 85 | } else if (nativeGzipDecode) { 86 | unpacked = nativeGzipDecode(new Uint8Array(data)); 87 | } else { 88 | const msg = `no brotli decoder available to decode WOFF2 font`; 89 | if (font.onerror) font.onerror(msg); 90 | throw new Error(msg); 91 | } 92 | 93 | view = new DataView(unpacked.buffer); 94 | } 95 | // uncompressed data. 96 | else { 97 | offset = entry.offset; 98 | } 99 | return createTable( 100 | woff.tables, 101 | { tag: entry.tag, offset, length: entry.origLength }, 102 | view 103 | ); 104 | }); 105 | }); 106 | } 107 | 108 | export { WOFF }; 109 | -------------------------------------------------------------------------------- /src/opentype/woff2.js: -------------------------------------------------------------------------------- 1 | import { SimpleTable } from "./tables/simple-table.js"; 2 | import lazy from "../lazy.js"; 3 | 4 | const brotliDecode = globalThis.unbrotli; 5 | let nativeBrotliDecode = undefined; 6 | 7 | if (!brotliDecode) { 8 | import("zlib").then((zlib) => { 9 | nativeBrotliDecode = (buffer) => zlib.brotliDecompressSync(buffer); 10 | }); 11 | } 12 | 13 | /** 14 | * The WOFF2 header 15 | * 16 | * See https://www.w3.org/TR/WOFF2 for WOFF2 information 17 | * See https://docs.microsoft.com/en-us/typography/opentype/spec/overview for font information 18 | */ 19 | class WOFF2 extends SimpleTable { 20 | constructor(font, dataview, createTable) { 21 | const { p } = super({ offset: 0, length: 48 }, dataview, `woff2`); 22 | this.signature = p.tag; 23 | this.flavor = p.uint32; 24 | this.length = p.uint32; 25 | this.numTables = p.uint16; 26 | p.uint16; // why woff2 even has any reserved bytes is a complete mystery. But it does. 27 | this.totalSfntSize = p.uint32; 28 | this.totalCompressedSize = p.uint32; 29 | this.majorVersion = p.uint16; 30 | this.minorVersion = p.uint16; 31 | this.metaOffset = p.uint32; 32 | this.metaLength = p.uint32; 33 | this.metaOrigLength = p.uint32; 34 | this.privOffset = p.uint32; 35 | this.privLength = p.uint32; 36 | 37 | p.verifyLength(); 38 | 39 | // parse the dictionary 40 | this.directory = [...new Array(this.numTables)].map( 41 | (_) => new Woff2TableDirectoryEntry(p) 42 | ); 43 | let dictOffset = p.currentPosition; // = start of CompressedFontData block 44 | 45 | // compute table byte offsets in the decompressed data 46 | this.directory[0].offset = 0; 47 | this.directory.forEach((e, i) => { 48 | let next = this.directory[i + 1]; 49 | if (next) { 50 | next.offset = 51 | e.offset + 52 | (e.transformLength !== undefined ? e.transformLength : e.origLength); 53 | } 54 | }); 55 | 56 | // then decompress the original data and lazy-bind 57 | let decoded; 58 | let buffer = dataview.buffer.slice(dictOffset); 59 | 60 | if (brotliDecode) { 61 | decoded = brotliDecode(new Uint8Array(buffer)); 62 | } else if (nativeBrotliDecode) { 63 | decoded = new Uint8Array(nativeBrotliDecode(buffer)); 64 | } else { 65 | const msg = `no brotli decoder available to decode WOFF2 font`; 66 | if (font.onerror) font.onerror(msg); 67 | throw new Error(msg); 68 | } 69 | 70 | buildWoff2LazyLookups(this, decoded, createTable); 71 | } 72 | } 73 | 74 | /** 75 | * WOFF2 Table Directory Entry 76 | */ 77 | class Woff2TableDirectoryEntry { 78 | constructor(p) { 79 | this.flags = p.uint8; 80 | 81 | const tagNumber = (this.tagNumber = this.flags & 63); 82 | if (tagNumber === 63) { 83 | this.tag = p.tag; 84 | } else { 85 | this.tag = getWOFF2Tag(tagNumber); 86 | } 87 | 88 | /* 89 | "Bits 6 and 7 indicate the preprocessing transformation version number (0-3) 90 | that was applied to each table. For all tables in a font, except for 'glyf' 91 | and 'loca' tables, transformation version 0 indicates the null transform 92 | where the original table data is passed directly to the Brotli compressor 93 | for inclusion in the compressed data stream. For 'glyf' and 'loca' tables, 94 | transformation version 3 indicates the null transform" 95 | */ 96 | const transformVersion = (this.transformVersion = (this.flags & 192) >> 6); 97 | let hasTransforms = transformVersion !== 0; 98 | if (this.tag === `glyf` || this.tag === `loca`) { 99 | hasTransforms = this.transformVersion !== 3; 100 | } 101 | 102 | this.origLength = p.uint128; 103 | if (hasTransforms) { 104 | this.transformLength = p.uint128; 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Build late-evaluating properties for each table in a 111 | * woff2 font, so that accessing a table via the 112 | * font.opentype.tables.tableName property kicks off 113 | * a table parse on first access. 114 | * 115 | * @param {*} woff2 the woff2 font object 116 | * @param {decoded} the original (decompressed) SFNT data 117 | * @param {createTable} the opentype table builder function 118 | */ 119 | function buildWoff2LazyLookups(woff2, decoded, createTable) { 120 | woff2.tables = {}; 121 | woff2.directory.forEach((entry) => { 122 | lazy(woff2.tables, entry.tag.trim(), () => { 123 | const start = entry.offset; 124 | const end = 125 | start + 126 | (entry.transformLength ? entry.transformLength : entry.origLength); 127 | const data = new DataView(decoded.slice(start, end).buffer); 128 | try { 129 | return createTable( 130 | woff2.tables, 131 | { tag: entry.tag, offset: 0, length: entry.origLength }, 132 | data 133 | ); 134 | } catch (e) { 135 | console.error(e); 136 | } 137 | }); 138 | }); 139 | } 140 | 141 | /** 142 | * WOFF2 uses a numbered tag registry, such that only unknown tables require a 4 byte tag 143 | * in the WOFF directory entry struct. Everything else uses a uint8. Nice and tidy. 144 | * @param {*} flag 145 | */ 146 | function getWOFF2Tag(flag) { 147 | return [ 148 | `cmap`, 149 | `head`, 150 | `hhea`, 151 | `hmtx`, 152 | `maxp`, 153 | `name`, 154 | `OS/2`, 155 | `post`, 156 | `cvt `, 157 | `fpgm`, 158 | `glyf`, 159 | `loca`, 160 | `prep`, 161 | `CFF `, 162 | `VORG`, 163 | `EBDT`, 164 | `EBLC`, 165 | `gasp`, 166 | `hdmx`, 167 | `kern`, 168 | `LTSH`, 169 | `PCLT`, 170 | `VDMX`, 171 | `vhea`, 172 | `vmtx`, 173 | `BASE`, 174 | `GDEF`, 175 | `GPOS`, 176 | `GSUB`, 177 | `EBSC`, 178 | `JSTF`, 179 | `MATH`, 180 | `CBDT`, 181 | `CBLC`, 182 | `COLR`, 183 | `CPAL`, 184 | `SVG `, 185 | `sbix`, 186 | `acnt`, 187 | `avar`, 188 | `bdat`, 189 | `bloc`, 190 | `bsln`, 191 | `cvar`, 192 | `fdsc`, 193 | `feat`, 194 | `fmtx`, 195 | `fvar`, 196 | `gvar`, 197 | `hsty`, 198 | `just`, 199 | `lcar`, 200 | `mort`, 201 | `morx`, 202 | `opbd`, 203 | `prop`, 204 | `trak`, 205 | `Zapf`, 206 | `Silf`, 207 | `Glat`, 208 | `Gloc`, 209 | `Feat`, 210 | `Sill`, 211 | ][flag & 63]; 212 | } 213 | 214 | export { WOFF2 }; 215 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | const startDate = new Date(`1904-01-01T00:00:00+0000`).getTime(); 2 | 3 | /** 4 | * Convert an array of uint8 char into a proper string. 5 | * 6 | * @param {uint8[]} data 7 | */ 8 | function asText(data) { 9 | return Array.from(data) 10 | .map((v) => String.fromCharCode(v)) 11 | .join(``); 12 | } 13 | 14 | /** 15 | * A data parser for table data, with auto-advancing pointer. 16 | */ 17 | class Parser { 18 | constructor(dict, dataview, name) { 19 | this.name = (name || dict.tag || ``).trim(); 20 | this.length = dict.length; 21 | this.start = dict.offset; 22 | this.offset = 0; 23 | this.data = dataview; 24 | 25 | [ 26 | `getInt8`, 27 | `getUint8`, 28 | `getInt16`, 29 | `getUint16`, 30 | `getInt32`, 31 | `getUint32`, 32 | `getBigInt64`, 33 | `getBigUint64`, 34 | ].forEach((name) => { 35 | let fn = name.replace(/get(Big)?/, "").toLowerCase(); 36 | let increment = parseInt(name.replace(/[^\d]/g, "")) / 8; 37 | Object.defineProperty(this, fn, { 38 | get: () => this.getValue(name, increment), 39 | }); 40 | }); 41 | } 42 | 43 | get currentPosition() { 44 | return this.start + this.offset; 45 | } 46 | 47 | set currentPosition(position) { 48 | this.start = position; 49 | this.offset = 0; 50 | } 51 | 52 | skip(n = 0, bits = 8) { 53 | this.offset += (n * bits) / 8; 54 | } 55 | 56 | getValue(type, increment) { 57 | let pos = this.start + this.offset; 58 | this.offset += increment; 59 | try { 60 | return this.data[type](pos); 61 | } catch (e) { 62 | console.error(`parser`, type, increment, this); 63 | console.error(`parser`, this.start, this.offset); 64 | throw e; 65 | } 66 | } 67 | 68 | flags(n) { 69 | if (n === 8 || n === 16 || n === 32 || n === 64) { 70 | return this[`uint${n}`] 71 | .toString(2) 72 | .padStart(n, 0) 73 | .split(``) 74 | .map((v) => v === "1"); 75 | } 76 | console.error( 77 | `Error parsing flags: flag types can only be 1, 2, 4, or 8 bytes long` 78 | ); 79 | console.trace(); 80 | } 81 | 82 | get tag() { 83 | const t = this.uint32; 84 | return asText([(t >> 24) & 255, (t >> 16) & 255, (t >> 8) & 255, t & 255]); 85 | } 86 | 87 | get fixed() { 88 | let major = this.int16; 89 | let minor = Math.round((1000 * this.uint16) / 65356); 90 | return major + minor / 1000; 91 | } 92 | 93 | get legacyFixed() { 94 | // Only used in the `maxp`, `post`, and `vhea` tables. 95 | let major = this.uint16; 96 | let minor = this.uint16.toString(16).padStart(4, 0); 97 | return parseFloat(`${major}.${minor}`); 98 | } 99 | 100 | get uint24() { 101 | // Why does DataView not have a 24 bit value getters? 102 | return (this.uint8 << 16) + (this.uint8 << 8) + this.uint8; 103 | } 104 | 105 | get uint128() { 106 | // I have no idea why the variable uint128 was chosen over a 107 | // fixed-width uint32, but it was, and so we need to decode it. 108 | let value = 0; 109 | for (let i = 0; i < 5; i++) { 110 | let byte = this.uint8; 111 | value = value * 128 + (byte & 127); 112 | if (byte < 128) break; 113 | } 114 | return value; 115 | } 116 | 117 | get longdatetime() { 118 | return new Date(startDate + 1000 * parseInt(this.int64.toString())); 119 | } 120 | 121 | // alias datatypes 122 | 123 | get fword() { 124 | return this.int16; 125 | } 126 | get ufword() { 127 | return this.uint16; 128 | } 129 | get Offset16() { 130 | return this.uint16; 131 | } 132 | get Offset32() { 133 | return this.uint32; 134 | } 135 | 136 | // "that weird datatype" 137 | get F2DOT14() { 138 | const bits = p.uint16; 139 | const integer = [0, 1, -2, -1][bits >> 14]; 140 | const fraction = bits & 0x3fff; 141 | return integer + fraction / 16384; 142 | } 143 | 144 | verifyLength() { 145 | if (this.offset != this.length) { 146 | console.error( 147 | `unexpected parsed table size (${this.offset}) for "${this.name}" (expected ${this.length})` 148 | ); 149 | } 150 | } 151 | 152 | /** 153 | * Read an entire data block. 154 | */ 155 | readBytes(n = 0, position = 0, bits = 8, signed = false) { 156 | n = n || this.length; 157 | if (n === 0) return []; 158 | 159 | if (position) this.currentPosition = position; 160 | 161 | const fn = `${signed ? `` : `u`}int${bits}`, 162 | slice = []; 163 | while (n--) slice.push(this[fn]); 164 | return slice; 165 | } 166 | } 167 | 168 | /** 169 | * ... docs go here ... 170 | */ 171 | class ParsedData { 172 | constructor(parser) { 173 | const pGetter = { enumerable: false, get: () => parser }; 174 | Object.defineProperty(this, `parser`, pGetter); 175 | 176 | const start = parser.currentPosition; 177 | const startGetter = { enumerable: false, get: () => start }; 178 | Object.defineProperty(this, `start`, startGetter); 179 | } 180 | 181 | load(struct) { 182 | Object.keys(struct).forEach((p) => { 183 | let props = Object.getOwnPropertyDescriptor(struct, p); 184 | if (props.get) { 185 | this[p] = props.get.bind(this); 186 | } else if (props.value !== undefined) { 187 | this[p] = props.value; 188 | } 189 | }); 190 | if (this.parser.length) { 191 | this.parser.verifyLength(); 192 | } 193 | } 194 | } 195 | 196 | export { Parser, ParsedData }; 197 | -------------------------------------------------------------------------------- /src/utils/fontface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Guess the font's CSS "format" by analysing the asset path. 3 | */ 4 | function getFontCSSFormat(path, errorOnStyle) { 5 | let pos = path.lastIndexOf(`.`); 6 | let ext = (path.substring(pos + 1) || ``).toLowerCase(); 7 | 8 | let format = { 9 | ttf: `truetype`, 10 | otf: `opentype`, 11 | woff: `woff`, 12 | woff2: `woff2`, 13 | }[ext]; 14 | 15 | if (format) return format; 16 | 17 | let msg = { 18 | eot: `The .eot format is not supported: it died in January 12, 2016, when Microsoft retired all versions of IE that didn't already support WOFF.`, 19 | svg: `The .svg format is not supported: SVG fonts (not to be confused with OpenType with embedded SVG) were so bad we took the entire fonts chapter out of the SVG specification again.`, 20 | fon: `The .fon format is not supported: this is an ancient Windows bitmap font format.`, 21 | ttc: `Based on the current CSS specification, font collections are not (yet?) supported.`, 22 | }[ext]; 23 | 24 | if (!msg) msg = `${path} is not a known webfont format.`; 25 | 26 | if (errorOnStyle) { 27 | // hard stop if the user wants stylesheet errors to count as true errors, 28 | throw new Error(msg); 29 | } else { 30 | // otherwise, only leave a warning in the output log. 31 | console.warn(`Could not load font: ${msg}`); 32 | } 33 | } 34 | 35 | /** 36 | * Create an @font-face stylesheet for browser use. 37 | */ 38 | async function setupFontFace(name, url, options = {}) { 39 | if (!globalThis.document) return; 40 | 41 | let format = getFontCSSFormat(url, options.errorOnStyle); 42 | if (!format) return; 43 | 44 | let style = document.createElement(`style`); 45 | style.className = `injected-by-Font-js`; 46 | 47 | let rules = []; 48 | if (options.styleRules) { 49 | rules = Object.entries(options.styleRules).map( 50 | ([key, value]) => `${key}: ${value};` 51 | ); 52 | } 53 | 54 | style.textContent = ` 55 | @font-face { 56 | font-family: "${name}"; 57 | ${rules.join(`\n\t`)} 58 | src: url("${url}") format("${format}"); 59 | }`; 60 | globalThis.document.head.appendChild(style); 61 | return style; 62 | } 63 | 64 | export { setupFontFace }; 65 | -------------------------------------------------------------------------------- /src/utils/shim-fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A shim for the Fetch API. If not defined, we assume we're running 3 | * in Node.js and shim the fetch function using the `fs` module. 4 | */ 5 | 6 | let fetchFunction = globalThis.fetch; 7 | 8 | if (!fetchFunction) { 9 | let backlog = []; 10 | 11 | fetchFunction = globalThis.fetch = (...args) => { 12 | return new Promise((resolve, reject) => { 13 | backlog.push({ args, resolve, reject }); 14 | }); 15 | }; 16 | 17 | import("fs") 18 | .then((fs) => { 19 | fetchFunction = globalThis.fetch = async function (path) { 20 | return new Promise((resolve, reject) => { 21 | fs.readFile(path, (err, data) => { 22 | if (err) return reject(err); 23 | resolve({ 24 | ok: true, 25 | arrayBuffer: () => data.buffer, 26 | }); 27 | }); 28 | }); 29 | }; 30 | 31 | while (backlog.length) { 32 | let instruction = backlog.shift(); 33 | fetchFunction(...instruction.args) 34 | .then((data) => instruction.resolve(data)) 35 | .catch((err) => instruction.reject(err)); 36 | } 37 | }) 38 | .catch((err) => { 39 | console.error(err); 40 | throw new Error( 41 | `lib-font cannot run unless either the Fetch API or Node's filesystem module is available.` 42 | ); 43 | }); 44 | } 45 | 46 | export default "shim activated"; 47 | -------------------------------------------------------------------------------- /src/utils/validator.js: -------------------------------------------------------------------------------- 1 | // Known font header byte sequences 2 | const TTF = [0x00, 0x01, 0x00, 0x00]; 3 | const OTF = [0x4f, 0x54, 0x54, 0x4f]; // "OTTO" 4 | const WOFF = [0x77, 0x4f, 0x46, 0x46]; // "wOFF" 5 | const WOFF2 = [0x77, 0x4f, 0x46, 0x32]; // "wOF2" 6 | 7 | /** 8 | * Array matching function 9 | */ 10 | function match(ar1, ar2) { 11 | if (ar1.length !== ar2.length) return; 12 | for (let i = 0; i < ar1.length; i++) { 13 | if (ar1[i] !== ar2[i]) return; 14 | } 15 | return true; 16 | } 17 | 18 | /** 19 | * verify a bytestream, based on the values we know 20 | * should be found at the first four bytes. 21 | */ 22 | function validFontFormat(dataview) { 23 | const LEAD_BYTES = [ 24 | dataview.getUint8(0), 25 | dataview.getUint8(1), 26 | dataview.getUint8(2), 27 | dataview.getUint8(3), 28 | ]; 29 | 30 | if (match(LEAD_BYTES, TTF) || match(LEAD_BYTES, OTF)) return `SFNT`; 31 | if (match(LEAD_BYTES, WOFF)) return `WOFF`; 32 | if (match(LEAD_BYTES, WOFF2)) return `WOFF2`; 33 | } 34 | 35 | export { validFontFormat }; 36 | -------------------------------------------------------------------------------- /testing/browser/matrix/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LibFont unit tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |
loading results
13 | 14 | 15 | -------------------------------------------------------------------------------- /testing/browser/puppeteer.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | let failed = false; 4 | 5 | (async () => { 6 | const browser = await puppeteer.launch(); 7 | const page = await browser.newPage(); 8 | 9 | try { 10 | await page.goto("http://localhost:8000/"); 11 | 12 | await page.waitForSelector("#finished"); 13 | 14 | // count how many tests failed 15 | const failures = await page.evaluate((qs) => { 16 | return Array.from(document.querySelectorAll(qs)).map((e) => 17 | e.textContent.trim() 18 | ); 19 | }, `.fail`); 20 | 21 | if (failures.length) { 22 | failed = true; 23 | console.error(`${failures.length} browser tests failed.\n`); 24 | failures.forEach((f) => console.error(f)); 25 | console.log(``); 26 | } else { 27 | console.log(`All browser tests passed.\n`); 28 | } 29 | } catch (err) { 30 | console.error(err); 31 | } finally { 32 | try { 33 | await page.goto("http://localhost:8000/shutdown"); 34 | } catch (err) { 35 | console.error(err); 36 | } 37 | await browser.close(); 38 | } 39 | 40 | process.exit(failed ? 1 : 0); 41 | })(); 42 | -------------------------------------------------------------------------------- /testing/browser/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import http from "http"; 3 | const app = express(); 4 | 5 | app.use(express.static(`.`)); 6 | app.get(`/shutdown`, (_req,res) => { 7 | res.json({ ok: true }); 8 | server.close(() => { 9 | process.exit(0); 10 | }); 11 | }); 12 | 13 | const server = http.createServer(app).listen(8000); 14 | -------------------------------------------------------------------------------- /testing/browser/test.js: -------------------------------------------------------------------------------- 1 | import "./tests/test.otf.js"; 2 | import "./tests/test.ttf.js"; 3 | -------------------------------------------------------------------------------- /testing/browser/tests/assert.js: -------------------------------------------------------------------------------- 1 | let rollout = typeof document === "undefined" ? false : document.body; 2 | let logPad = []; 3 | 4 | function arrayEqual(a,b) { 5 | if(!b.map) return false; 6 | if (a.length !== b.length) return false; 7 | return a.every((e,i) => equal(e, b[i])); 8 | } 9 | 10 | function equal(a, b) { 11 | if (a.map) return arrayEqual(a,b); 12 | return (a === b); 13 | } 14 | 15 | function note(msg, style, classes) { 16 | if (!rollout) { 17 | return console.log(msg); 18 | } 19 | let div = document.createElement('div') 20 | div.textContent = msg; 21 | if (style) div.setAttribute(`style`, style); 22 | if (classes) div.setAttribute(`class`, classes); 23 | rollout.appendChild(div); 24 | } 25 | 26 | function heading(msg) { 27 | note(msg, `font-weight: bold; font-size: 1.5em; padding: 0.5em 0`); 28 | } 29 | 30 | function pass(why) { 31 | note(`${logPad.join('')}${why}`, false, `pass`); 32 | } 33 | 34 | function fail (a, b, why) { 35 | note(`${logPad.join('')}${why} is false: ${a} is not ${b}`, false, `fail`); 36 | } 37 | 38 | function assertEqual(a, b, why) { 39 | if (equal(a,b)) { pass(why); } 40 | else { fail(a, b, why); } 41 | } 42 | 43 | function assertNotEqual(a, b, why) { 44 | if (equal(a,b)) { fail(a, b, why); } 45 | else { pass(why); } 46 | } 47 | 48 | function indent() { 49 | logPad.push('\t'); 50 | } 51 | 52 | function unindent(full) { 53 | logPad.pop(); 54 | if(full) logPad = []; 55 | } 56 | 57 | export { heading, indent, unindent, assertEqual, assertNotEqual }; 58 | -------------------------------------------------------------------------------- /testing/browser/tests/test.otf.js: -------------------------------------------------------------------------------- 1 | import { 2 | heading, 3 | indent, 4 | unindent, 5 | assertEqual, 6 | assertNotEqual, 7 | } from "./assert.js"; 8 | 9 | import { testSFNT } from "./test-SFNT.js"; 10 | 11 | const name = `Source Code Pro Regular (otf)`; 12 | const font = new Font(name); 13 | 14 | font.onload = () => { 15 | heading(`Plain OTF tests`); 16 | 17 | unindent(true); 18 | 19 | const fontName = font.name; 20 | assertEqual(fontName, name, `Font object has correct name property`); 21 | 22 | const SFNT = font.opentype; 23 | assertNotEqual(SFNT, undefined, `SFNT EXISTS`); 24 | 25 | indent(); 26 | 27 | assertEqual(SFNT.version, 1330926671, `Version is OTTO`); 28 | assertEqual(SFNT.numTables, 15, `There are 15 tables in this font`); 29 | 30 | const expected = [ 31 | "BASE", 32 | "CFF ", 33 | "DSIG", 34 | "GDEF", 35 | "GPOS", 36 | "GSUB", 37 | "OS/2", 38 | "SVG ", 39 | "cmap", 40 | "head", 41 | "hhea", 42 | "hmtx", 43 | "maxp", 44 | "name", 45 | "post", 46 | ]; 47 | 48 | assertEqual( 49 | SFNT.directory.map((d) => d.tag), 50 | expected, 51 | `tables: "${expected.join(`", "`)}"` 52 | ); 53 | 54 | assertEqual(SFNT.searchRange, 128, `Correct searchRange`); 55 | assertEqual(SFNT.entrySelector, 3, `Correct entrySelector`); 56 | assertEqual(SFNT.rangeShift, 112, `Correct rangeShift`); 57 | 58 | testSFNT(SFNT); 59 | 60 | assertEqual(font.supports("a"), true, `font supports the Latin lowercase A`); 61 | assertEqual( 62 | font.supports("Ɔ"), 63 | false, 64 | `font does not support the Latin capital letter open O` 65 | ); 66 | assertEqual( 67 | font.supports("〆"), 68 | false, 69 | `font does not support the Japanese EOL marker` 70 | ); 71 | 72 | document.dispatchEvent( 73 | new CustomEvent(`testend`, { detail: { test: `otf` } }) 74 | ); 75 | }; 76 | 77 | font.src = `../fonts/SourceCodePro/SourceCodePro-Regular.otf`; 78 | 79 | export { font }; 80 | -------------------------------------------------------------------------------- /testing/browser/tests/test.ttf.js: -------------------------------------------------------------------------------- 1 | import { 2 | heading, 3 | indent, 4 | unindent, 5 | assertEqual, 6 | assertNotEqual, 7 | } from "./assert.js"; 8 | 9 | import { testSFNT } from "./test-SFNT.js"; 10 | 11 | const name = `Source Code Pro Regular (ttf)`; 12 | const font = new Font(name); 13 | 14 | font.onload = () => { 15 | heading(`Plain TTF tests`); 16 | 17 | unindent(true); 18 | 19 | const fontName = font.name; 20 | assertEqual(fontName, name, `Font object has correct name property`); 21 | 22 | const SFNT = font.opentype; 23 | assertNotEqual(SFNT, undefined, `SFNT EXISTS`); 24 | 25 | indent(); 26 | 27 | assertEqual(SFNT.version, 0x00010000, `Version is 0x0001.0x0000`); 28 | assertEqual(SFNT.numTables, 20, `There are 20 tables in this font`); 29 | 30 | const expected = [ 31 | "BASE", 32 | "DSIG", 33 | "GDEF", 34 | "GPOS", 35 | "GSUB", 36 | "OS/2", 37 | "SVG ", 38 | "cmap", 39 | "cvt ", 40 | "fpgm", 41 | "gasp", 42 | "glyf", 43 | "head", 44 | "hhea", 45 | "hmtx", 46 | "loca", 47 | "maxp", 48 | "name", 49 | "post", 50 | "prep", 51 | ]; 52 | assertEqual( 53 | SFNT.directory.map((d) => d.tag), 54 | expected, 55 | `tables: "${expected.join(`", "`)}"` 56 | ); 57 | 58 | assertEqual(SFNT.searchRange, 256, `Correct searchRange`); 59 | assertEqual(SFNT.entrySelector, 4, `Correct entrySelector`); 60 | assertEqual(SFNT.rangeShift, 64, `Correct rangeShift`); 61 | 62 | testSFNT(SFNT, true); 63 | 64 | assertEqual(font.supports("a"), true, `font supports the Latin lowercase A`); 65 | assertEqual( 66 | font.supports("Ɔ"), 67 | false, 68 | `font does not support the Latin capital letter open O` 69 | ); 70 | assertEqual( 71 | font.supports("〆"), 72 | false, 73 | `font does not support the Japanese EOL marker` 74 | ); 75 | 76 | document.dispatchEvent( 77 | new CustomEvent(`testend`, { detail: { test: `ttf` } }) 78 | ); 79 | }; 80 | 81 | font.src = `../fonts/SourceCodePro/SourceCodePro-Regular.ttf`; 82 | 83 | export { font }; 84 | -------------------------------------------------------------------------------- /testing/gsub-recipe.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../lib-font.js"; 2 | 3 | let font = new Font("test"); 4 | font.onerror = evt => console.error(evt); 5 | font.onload = evt => load(); 6 | font.src = "..."; 7 | 8 | 9 | function load() { 10 | const { GSUB } = font.opentype.tables; 11 | 12 | let scripts = GSUB.getSupportedScripts(); 13 | 14 | scripts.forEach((script) => { 15 | let langsys = GSUB.getSupportedLangSys(script); 16 | 17 | langsys.forEach((lang) => { 18 | let langSysTable = GSUB.getLangSysTable(script, lang); 19 | let features = GSUB.getFeatures(langSysTable); 20 | 21 | features.forEach((feature) => { 22 | const lookupIDs = feature.lookupListIndices; 23 | 24 | console.log(script, lang, feature.featureTag, lookupIDs); 25 | }); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /testing/manual/custom/brush-grotesk-woff2-gsub.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../../lib-font.js"; 2 | 3 | const font = new Font("woff2 testing"); 4 | font.src = `./fonts/broken/BrushPosterGrotesk.woff2`; 5 | font.onerror = (evt) => console.error(evt); 6 | font.onload = (evt) => { 7 | let font = evt.detail.font; 8 | 9 | const { GSUB } = font.opentype.tables; 10 | let scripts = GSUB.getSupportedScripts(); 11 | 12 | scripts.forEach((script) => { 13 | let langsys = GSUB.getSupportedLangSys(script); 14 | 15 | langsys.forEach((lang) => { 16 | let langSysTable = GSUB.getLangSysTable(script, lang); 17 | let features = GSUB.getFeatures(langSysTable); 18 | 19 | features.forEach((feature) => { 20 | const lookupIDs = feature.lookupListIndices; 21 | 22 | lookupIDs.forEach((id) => { 23 | const lookup = GSUB.getLookup(id); 24 | 25 | const cnt = lookup.subTableCount; 26 | const s = cnt !== 1 ? "s" : ""; 27 | console.log( 28 | `lookup type ${lookup.lookupType} in ${lang}, lookup ${id}, ${cnt} subtable${s}` 29 | ); 30 | 31 | /* 32 | // Only dump lookup type 6 for DFLT/dflt 33 | if (lookup.lookupType === 6 && lang === "dflt") { 34 | 35 | for(let i=0; i 0) 44 | subtable.backtrackCoverageOffsets.forEach((offset, id) => { 45 | let coverage = subtable.getCoverageFromOffset(offset); 46 | console.log(`backtrack coverage ${id+1}:`, coverage); 47 | }); 48 | 49 | if (subtable.lookaheadGlyphCount > 0) 50 | subtable.lookaheadCoverageOffsets.forEach((offset, id) => { 51 | let coverage = subtable.getCoverageFromOffset(offset); 52 | console.log(`lookahead coverage ${id+1}:`, coverage); 53 | }); 54 | 55 | subtable.seqLookupRecords.forEach(slRecord => { 56 | console.log(`sequence lookup record:`, slRecord); 57 | }); 58 | } 59 | } 60 | */ 61 | }); 62 | }); 63 | }); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /testing/manual/custom/castoro-parsing.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../../lib-font.js"; 2 | 3 | const font = new Font("castoro"); 4 | font.onload = (evt) => { 5 | try { 6 | testFont(evt.detail.font); 7 | } catch (e) { 8 | console.error(e); 9 | } 10 | }; 11 | 12 | font.src = "./fonts/issue-123/Castoro-Regular.woff2"; 13 | 14 | function testFont(font) { 15 | const { GSUB } = font.opentype.tables; 16 | const scripts = GSUB.getSupportedScripts(); 17 | let allGlyphs = {}; 18 | 19 | scripts.forEach((script) => { 20 | let langsys = GSUB.getSupportedLangSys(script); 21 | 22 | allGlyphs[script] = {}; 23 | 24 | langsys.forEach((lang) => { 25 | let langSysTable = GSUB.getLangSysTable(script, lang); 26 | let features = GSUB.getFeatures(langSysTable); 27 | 28 | allGlyphs[script][lang] = {}; 29 | 30 | features.forEach((feature) => { 31 | const lookupIDs = feature.lookupListIndices; 32 | allGlyphs[script][lang][feature.featureTag] = {}; 33 | allGlyphs[script][lang][feature.featureTag]["lookups"] = []; 34 | 35 | lookupIDs.forEach((id) => { 36 | const lookup = GSUB.getLookup(id); 37 | 38 | lookup.subtableOffsets.forEach((_, i) => { 39 | const subtable = lookup.getSubTable(i); 40 | console.log( 41 | `Getting lookup ${`${id}`.padStart(2, ' ')}, subtable ${i}, lookuptype ${subtable.type}, format ${subtable.substFormat}, feature ${feature.featureTag}, script ${script}, lang ${lang}` 42 | ); 43 | }); 44 | }); 45 | }); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /testing/manual/custom/flaticon-parsing.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../../lib-font.js"; 2 | 3 | const font = new Font("flaticon"); 4 | font.onload = (evt) => { 5 | try { testFont(evt.detail.font); } 6 | catch (e) { console.error(e); } 7 | }; 8 | 9 | font.src = "./fonts/proprietary/helvetica.woff"; 10 | 11 | function testFont(font) { 12 | const { directory, tables } = font.opentype; 13 | 14 | const { name } = tables; 15 | name.nameRecords.forEach((record) => { 16 | const str = record.string; 17 | console.log(str); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /testing/manual/custom/issue-127.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../../lib-font.js"; 2 | 3 | const font = new Font("SVG testing"); 4 | font.src = `./fonts/issue-127/Abelone-FREE.otf`; 5 | font.onerror = (evt) => console.error(evt); 6 | font.onload = (evt) => { 7 | const font = evt.detail.font; 8 | const { tables } = font.opentype; 9 | const SVGTable = tables["SVG"]; 10 | console.log(SVGTable); 11 | }; 12 | -------------------------------------------------------------------------------- /testing/manual/custom/issue-130.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../../lib-font.js"; 2 | 3 | // Create a font object 4 | const myFont = new Font(`Lib Font Test Font`); 5 | 6 | // Assign event handling (.addEventListener version supported too, of course) 7 | myFont.onerror = (evt) => console.error(evt); 8 | myFont.onload = (evt) => doSomeFontThings(evt); 9 | 10 | // Kick off the font load by setting a source file, exactly as you would 11 | myFont.src = `./fonts/issue-130/ABeeZee-Regular.ttf`; 12 | 13 | // When the font's up and loaded in, let's do some testing! 14 | function doSomeFontThings(evt) { 15 | const font = evt.detail.font; 16 | const GSUB = font.opentype.tables.GSUB; 17 | 18 | const scripts = GSUB.getSupportedScripts(); 19 | let allGlyphs = {}; 20 | 21 | scripts.forEach((script, scriptId) => { 22 | let langsys = GSUB.getSupportedLangSys(script); 23 | 24 | allGlyphs[script] = {}; 25 | 26 | console.log(`script id ${scriptId}`); 27 | 28 | langsys.forEach((lang, langId) => { 29 | let langSysTable = GSUB.getLangSysTable(script, lang); 30 | let features = GSUB.getFeatures(langSysTable); 31 | 32 | allGlyphs[script][lang] = {}; 33 | 34 | console.log(`|--lang id ${langId}`); 35 | 36 | features.forEach((feature, featureId) => { 37 | const lookupIDs = feature.lookupListIndices; 38 | allGlyphs[script][lang][feature.featureTag] = {}; 39 | allGlyphs[script][lang][feature.featureTag]["lookups"] = []; 40 | 41 | console.log(`| |--feature id ${featureId}`); 42 | 43 | lookupIDs.forEach((id, lookupId) => { 44 | const lookup = GSUB.getLookup(id); 45 | 46 | console.log(`| | |--lookup id ${lookupId}`); 47 | 48 | lookup.subtableOffsets.forEach((_, i) => { 49 | const subtable = lookup.getSubTable(i); 50 | 51 | console.log( 52 | `| | | |--subtable id ${i} (type ${lookup.lookupType}, substFormat ${subtable.substFormat})` 53 | ); 54 | 55 | if (lookup.lookupType === 6) { 56 | const inputGlyphCount = subtable.inputGlyphCount; 57 | 58 | console.log( 59 | `| | | | |--subtable glyph count: ${inputGlyphCount}` 60 | ); // ❌ 61 | 62 | const chainSubRuleSetCount = subtable.chainSubRuleSetCount; 63 | console.log( 64 | `| | | | |--chainSubRuleSetCount: ${chainSubRuleSetCount}` 65 | ); 66 | 67 | for (let css = 0; css < chainSubRuleSetCount; css++) { 68 | const chainSubRuleSet = subtable.getChainSubRuleSet(css); 69 | const chainSubRuleCount = chainSubRuleSet.chainSubRuleCount; 70 | console.log( 71 | `| | | | | |--chainSubRuleCount for set ${css}: ${chainSubRuleCount}` 72 | ); 73 | 74 | for (let csr = 0; csr < chainSubRuleCount; csr++) { 75 | const chainSubRule = chainSubRuleSet.getSubRule(csr); 76 | const inputGlyphCount = chainSubRule.inputGlyphCount; 77 | console.log( 78 | `| | | | | | |--inputGlyphCount for rule ${csr}: ${inputGlyphCount}` 79 | ); 80 | } 81 | } 82 | } 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /testing/manual/custom/issue-85.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { Font } from "../../../lib-font.js"; 3 | 4 | const font = new Font("testfont"); 5 | font.src = "../../../fonts/IBMPlexSansThai-Light.ttf"; 6 | 7 | font.onload = (evt) => { 8 | let font = evt.detail.font; 9 | const { name } = font.opentype.tables; 10 | console.log(name); 11 | fs.writeFileSync(`test.out`, `name: ${name.get(9)}`, `utf-8`); 12 | }; -------------------------------------------------------------------------------- /testing/manual/custom/test.out: -------------------------------------------------------------------------------- 1 | name: Mike Abbink, Paul van der Laan, Pieter van Rosmalen, Ben Mitchell, Mark Fršmberg -------------------------------------------------------------------------------- /testing/manual/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LibFont unit tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Please open dev tools for now

13 | 14 | 15 | -------------------------------------------------------------------------------- /testing/manual/index.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../lib-font.js"; 2 | 3 | const font = new Font("woff2 testing"); 4 | font.onerror = (evt) => console.error(evt); 5 | font.onload = (evt) => { 6 | let font = evt.detail.font; 7 | 8 | Object.entries(font.opentype.tables).forEach(v => 9 | console.log(v[0]) 10 | ); 11 | 12 | // const { GSUB } = font.opentype.tables; 13 | // processGSUB(GSUB); 14 | }; 15 | 16 | const fonts = [ 17 | `./fonts/AthenaRuby_b018.ttf`, 18 | ]; 19 | 20 | font.src = fonts[0]; 21 | 22 | function processGSUB(GSUB) { 23 | let scripts = GSUB.getSupportedScripts(); 24 | 25 | scripts.forEach((script) => { 26 | let langsys = GSUB.getSupportedLangSys(script); 27 | 28 | langsys.forEach((lang) => { 29 | let langSysTable = GSUB.getLangSysTable(script, lang); 30 | let features = GSUB.getFeatures(langSysTable); 31 | 32 | features.forEach((feature) => { 33 | const lookupIDs = feature.lookupListIndices; 34 | 35 | lookupIDs.forEach((id) => { 36 | const lookup = GSUB.getLookup(id); 37 | const cnt = lookup.subTableCount; 38 | const s = cnt !== 1 ? "s" : ""; 39 | console.log( 40 | `lookup type ${lookup.lookupType} in ${lang}, lookup ${id}, ${cnt} subtable${s}` 41 | ); 42 | }); 43 | }); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /testing/node/athena.ruby.test.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../lib-font.js"; 2 | import { testGSUB } from "./gsub/test-gsub.js"; 3 | 4 | const font = new Font("athena ruby"); 5 | 6 | describe("Basic font testing", () => { 7 | beforeAll((done) => { 8 | font.onerror = (err) => { 9 | throw err; 10 | }; 11 | font.onload = async () => done(); 12 | font.src = "./fonts/AthenaRuby_b018.ttf"; 13 | }); 14 | 15 | test("font loaded", () => { 16 | expect(font.opentype).toBeDefined(); 17 | }); 18 | 19 | test("GSUB format 3 variant access", () => { 20 | testGSUB(font, { 21 | script: [], 22 | feature: [], 23 | lookup: [lookup3Alternates] 24 | }) 25 | }); 26 | }); 27 | 28 | 29 | function lookup3Alternates(font, script, lang, feature, lookupId, lookup) { 30 | if (lookup.lookupType !== 3) return; 31 | if (lookupId !== 15) return; 32 | 33 | // console.log(lookup); 34 | 35 | const subtable = lookup.getSubTable(0); 36 | const coverage = subtable.getCoverageTable(0); 37 | const altset = subtable.getAlternateSet(0); 38 | 39 | // console.log(subtable, coverage, altset); 40 | 41 | const getGlyphName = id => font.opentype.tables.post.getGlyphName(id); 42 | // console.log(getGlyphName(coverage.glyphArray[0]), `⇒`, altset.alternateGlyphIDs.map(getGlyphName)); 43 | 44 | // test here 45 | } 46 | -------------------------------------------------------------------------------- /testing/node/font-profiles/mehr-nastaliq.js: -------------------------------------------------------------------------------- 1 | export default { 2 | arab: { 3 | langsys: [`dflt`, `FAR `, `URD `], 4 | features: { 5 | dflt: { 6 | length: 7, 7 | lookups: { 8 | ccmp: [46], 9 | fina: [47], 10 | init: [48], 11 | isol: [50], 12 | medi: [0, 49], 13 | mset: [42, 44], 14 | rlig: [51, 52, 53], 15 | }, 16 | }, 17 | "FAR ": { 18 | length: 7, 19 | lookups: { 20 | ccmp: [46], 21 | fina: [47], 22 | init: [48], 23 | isol: [50], 24 | medi: [0, 49], 25 | mset: [42, 44], 26 | rlig: [51, 52, 53], 27 | }, 28 | }, 29 | "URD ": { 30 | length: 7, 31 | lookups: { 32 | ccmp: [46], 33 | fina: [47], 34 | init: [48], 35 | isol: [50], 36 | medi: [0, 49], 37 | mset: [42, 44], 38 | rlig: [51, 52, 53], 39 | }, 40 | }, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /testing/node/font-profiles/profiles.js: -------------------------------------------------------------------------------- 1 | import SourceCodePro from "./source-code-pro.js"; 2 | import AthenaRuby from "./athena-ruby.js"; 3 | import MehrNastaliq from "./mehr-nastaliq.js"; 4 | 5 | 6 | const profiles = { 7 | "source code pro": SourceCodePro, 8 | "athena ruby": AthenaRuby, 9 | "mehr nastaliq": MehrNastaliq, 10 | }; 11 | 12 | export { profiles }; 13 | -------------------------------------------------------------------------------- /testing/node/gsub/test-gsub.js: -------------------------------------------------------------------------------- 1 | import { expect } from "@jest/globals"; 2 | import { profiles } from "../font-profiles/profiles.js"; 3 | 4 | function testGSUB(font, tests) { 5 | const expectation = profiles[font.name]; 6 | 7 | const { cmap, name, GSUB } = font.opentype.tables; 8 | 9 | expect(GSUB).toBeDefined(); 10 | expect(cmap).toBeDefined(); 11 | expect(name).toBeDefined(); 12 | 13 | let scripts = GSUB.getSupportedScripts(); 14 | expect(scripts).toEqual(Object.keys(expectation)); 15 | 16 | scripts.forEach((script) => { 17 | tests.script.forEach(fn => fn(script)); 18 | 19 | let langsys = GSUB.getSupportedLangSys(script); 20 | 21 | expect(langsys).toEqual(expectation[script].langsys); 22 | 23 | langsys.forEach((lang) => { 24 | let langSysTable = GSUB.getLangSysTable(script, lang); 25 | let features = GSUB.getFeatures(langSysTable); 26 | let featureCount = features.length; 27 | 28 | expect(featureCount).toEqual(expectation[script].features[lang].length); 29 | 30 | features.forEach((feature) => { 31 | tests.feature.forEach(fn => fn(feature)); 32 | 33 | const lookupIDs = feature.lookupListIndices; 34 | 35 | const test = { 36 | script, 37 | lang, 38 | feature: feature.featureTag, 39 | lookupIDs 40 | }; 41 | 42 | const match = { 43 | script, 44 | lang, 45 | feature: feature.featureTag, 46 | lookupIDs: expectation[script].features[lang].lookups[feature.featureTag] 47 | }; 48 | 49 | expect(test).toEqual(match); 50 | 51 | lookupIDs.forEach((id) => { 52 | const lookup = GSUB.getLookup(id); 53 | tests.lookup.forEach(fn => fn(font, script, lang, feature.featureTag, id, lookup)); 54 | }); 55 | }); 56 | }); 57 | }); 58 | } 59 | 60 | export { testGSUB }; 61 | -------------------------------------------------------------------------------- /testing/node/issue-114/flaticon.test.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../../lib-font.js"; 2 | 3 | const font = new Font("flaticon"); 4 | 5 | describe("Basic font testing", () => { 6 | beforeAll((done) => { 7 | font.onerror = (err) => { 8 | throw err; 9 | }; 10 | font.onload = async () => done(); 11 | font.src = "./fonts/issue-114/Flaticon.woff2"; 12 | }); 13 | 14 | test("font loaded", () => { 15 | expect(font.opentype).toBeDefined(); 16 | }); 17 | 18 | test("has name table", () => { 19 | const { name } = font.opentype.tables; 20 | 21 | expect(name).toBeDefined(); 22 | expect(name.count).toBe(14); 23 | 24 | const ID3 = `FontForge 2.0 : Flaticon : 21-12-2019`; 25 | expect(name.get(3)).toBe(ID3); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /testing/node/mehr.nastaliq.test.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../lib-font.js"; 2 | import { testGSUB } from "./gsub/test-gsub.js"; 3 | 4 | const font = new Font("mehr nastaliq"); 5 | 6 | describe("Basic font testing", () => { 7 | beforeAll((done) => { 8 | font.onerror = (err) => { 9 | throw err; 10 | }; 11 | font.onload = async () => done(); 12 | font.src = "./fonts/MehrNastaliqWeb-Regular.ttf"; 13 | }); 14 | 15 | test("font loaded", () => { 16 | expect(font.opentype).toBeDefined(); 17 | }); 18 | 19 | test("GSUB format 6/8 functionality", () => { 20 | testGSUB(font, { 21 | script: [], 22 | feature: [], 23 | lookup: [ 24 | type6LookupTest, 25 | type8LookupTest, 26 | ] 27 | }) 28 | }); 29 | }); 30 | 31 | function type6LookupTest(font, script, langsys, feature, lookupId, lookup) { 32 | if (lookup.lookupType !== 6) return; 33 | // console.log(script, langsys, feature, lookupId); 34 | } 35 | 36 | function type8LookupTest(font, script, langsys, feature, lookupId, lookup) { 37 | if (lookup.lookupType !== 8) return; 38 | // console.log(script, langsys, feature, lookupId); 39 | } 40 | -------------------------------------------------------------------------------- /testing/node/source.code.pro.test.js: -------------------------------------------------------------------------------- 1 | import { Font } from "../../lib-font.js"; 2 | import { testGSUB } from "./gsub/test-gsub.js"; 3 | 4 | const font = new Font("source code pro"); 5 | let letterFor = function(){}; 6 | 7 | describe("Basic font testing", () => { 8 | beforeAll((done) => { 9 | font.onerror = (err) => { 10 | throw err; 11 | }; 12 | font.onload = async () => done(); 13 | font.src = "./fonts/SourceCodePro/SourceCodePro-Regular.ttf"; 14 | }); 15 | 16 | test("font loaded", () => { 17 | expect(font.opentype).toBeDefined(); 18 | 19 | letterFor = function(glyphid) { 20 | let reversed = font.opentype.tables.cmap.reverse(glyphid); 21 | return reversed.unicode ?? `[${glyphid}:??]`; 22 | }; 23 | }); 24 | 25 | test("Glyph support", () => { 26 | expect(font.supports(`f`)).toBe(true); 27 | expect(font.supports(`i`)).toBe(true); 28 | expect(font.supports(String.fromCharCode(0xffff))).toBe(false); 29 | }); 30 | 31 | test("HEAD table", () => { 32 | const head = font.opentype.tables.head; 33 | expect(head).toBeDefined(); 34 | 35 | expect(head.magicNumber).toBe(1594834165); 36 | expect(head.fontDirectionHint).toBe(2); 37 | expect(head.unitsPerEm).toBe(1000); 38 | expect(head.xMin).toBe(-193); 39 | expect(head.xMax).toBe(793); 40 | expect(head.yMin).toBe(-454); 41 | expect(head.yMax).toBe(1060); 42 | }); 43 | 44 | test("GSUB table", () => { 45 | testGSUB(font, { 46 | script: [], 47 | feature: [], 48 | lookup: [ 49 | oneForOneSubstitution, 50 | ligatureSubstitutions, 51 | ] 52 | }); 53 | }); 54 | }); 55 | 56 | 57 | function oneForOneSubstitution(font, script, lang, feature, lookupId, lookup) { 58 | if (lookup.lookupType !== 1) return; 59 | 60 | lookup.subtableOffsets.forEach((_, i) => { 61 | const subtable = lookup.getSubTable(i); 62 | const coverage = subtable.getCoverageTable(); 63 | let glyphs = coverage.glyphArray; 64 | if (!glyphs) { 65 | glyphs = coverage.rangeRecords.map( 66 | (r) => `${r.startGlyphID}-${r.endGlyphID}` 67 | ); 68 | } 69 | }); 70 | } 71 | 72 | 73 | function ligatureSubstitutions(font, script, lang, feature, lookupId, lookup) { 74 | if (lookup.lookupType !== 4) return; 75 | 76 | lookup.subtableOffsets.forEach((_, i) => { 77 | const subtable = lookup.getSubTable(i); 78 | const coverage = subtable.getCoverageTable(); 79 | 80 | subtable.ligatureSetOffsets.forEach((_, setIndex) => { 81 | const ligatureSet = subtable.getLigatureSet(setIndex); 82 | 83 | ligatureSet.ligatureOffsets.forEach((_, ligIndex) => { 84 | const ligatureTable = ligatureSet.getLigature(ligIndex); 85 | 86 | const sequence = [ 87 | coverage.glyphArray[setIndex], 88 | ...ligatureTable.componentGlyphIDs, 89 | ]; 90 | 91 | // console.log( 92 | // `ligature set [${setIndex}], ligature table [${ligIndex}]: ${script}[${lang}].${feature.featureTag}[${id}]: ligature (coverage:${coverage.coverageFormat}) [ ${ 93 | // sequence.map(letterFor).join(` + `) 94 | // } ] -> ${ 95 | // letterFor(ligatureTable.ligatureGlyph) 96 | // }` 97 | // ); 98 | }); 99 | }); 100 | }); 101 | } --------------------------------------------------------------------------------