├── .gitignore ├── README.md ├── cli.js ├── index.html ├── index.js ├── package.json ├── run_local_server.bat ├── src ├── font.js ├── glyphRangeParser.js ├── main.js └── renderer.js ├── test.js ├── webpack.config.js └── webpack └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /temp/ 3 | /glyphs/ 4 | /package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # font 2 | 3 | [📱 Try a glyph viewer right now in your browser!](https://hlorenzi.github.io/font-js) 4 | 5 | Utilities and a CLI for extracting glyphs and metadata from font files. 6 | 7 | Currently allows loading TrueType or OpenType fonts or font collections, 8 | and extracting metadata and glyph geometry. 9 | 10 | The command-line interface allows for extracting black-and-white, 11 | grayscale, and signed distance field PNG renderings, and JSON metadata. 12 | 13 | Run the command-line interface without arguments to check all the 14 | available options. 15 | 16 | Run via npx: `npx @hlorenzi/font` 17 | 18 | Install with: `npm install @hlorenzi/font` 19 | 20 | ## Command-line Examples 21 | 22 | ```shell 23 | # Extracts all glyphs from "arial.ttf" into PNG and JSON files. 24 | npx @hlorenzi/font arial.ttf -o "output/unicode_[unicode]" 25 | 26 | # Set glyph range. 27 | npx @hlorenzi/font arial.ttf -o "output/unicode_[unicode]" --glyphs="u+30..u+39" 28 | 29 | # Set output mode. 30 | npx @hlorenzi/font arial.ttf -o "output/unicode_[unicode]" --img-mode="png-sdf" 31 | ``` 32 | 33 | ## Package Example 34 | 35 | ```js 36 | import { FontCollection, GlyphRenderer } from "@hlorenzi/font" 37 | import fs from "fs" 38 | 39 | // Load font file. 40 | const fontBuffer = fs.readFileSync("arial.ttf") 41 | 42 | // Load font collection and get first font. 43 | const fontCollection = FontCollection.fromBytes(fontBuffer) 44 | const font = fontCollection.fonts[0] 45 | 46 | // Find glyph for Unicode character "a" and get its geometry, 47 | // simplifying each bézier curve into 100 line segments. 48 | const glyphIndex = font.getGlyphIndexForUnicode("a".charCodeAt(0)) 49 | const glyphGeometry = font.getGlyphGeometry(glyphIndex, 100) 50 | 51 | // Render into a black-and-white buffer with scale factor 1 EM = 30 pixels, 52 | // then crop empty borders and print to the console. 53 | const glyphImage = GlyphRenderer.render(glyphGeometry, 30).cropped() 54 | console.log(glyphImage.printToString()) 55 | ``` -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | import { FontCollection, Font, GlyphRenderer } from "./index.js" 2 | import { parseGlyphRange } from "./src/glyphRangeParser.js" 3 | import fs from "fs" 4 | import minimist from "minimist" 5 | import PNG from "pngjs" 6 | import path from "path" 7 | 8 | 9 | //import PackageJson from "./package.json" 10 | //console.log(PackageJson.name + " v" + PackageJson.version) 11 | 12 | 13 | const usage = 14 | ` 15 | Usage: 16 | font-inspect [options] 17 | 18 | Options: 19 | --glyphs (default "*") 20 | The list of glyphs to extract. 21 | You can use decimal glyph IDs (#1234), hex Unicode codepoints (U+1abcd), ranges (..), and commas. 22 | You can also use an asterisk (*) to specify all glyphs, and (U+*) to specify all Unicode codepoints. 23 | Example: "U+0..U+7f,#500..#650,U+1e000" 24 | 25 | --img-mode (default "png-grayscale") 26 | The image format to which glyphs will be rendered. 27 | Available formats: 28 | 29 | "none" 30 | Does not output image files. 31 | 32 | "png-binary" 33 | PNG file with a black-and-white rasterization of the glyph, 34 | where white corresponds to areas on the inside of contours. 35 | Can be combined with the '--use-alpha' option. 36 | 37 | "png-grayscale" 38 | PNG file with a 256-level grayscale rasterization of the glyph, 39 | where white corresponds to areas on the inside of contours. 40 | Can be combined with the '--use-alpha' and '--gamma' options. 41 | 42 | "png-sdf" 43 | PNG file with a 256-level grayscale signed distance field, 44 | mapped to colors using the '--sdf-min' and '--sdf-max' options. 45 | Can be combined with the '--use-alpha' option. 46 | 47 | --data-mode (default "json") 48 | The data format to which glyph metadata will be extracted. 49 | Available formats: 50 | 51 | "none" 52 | Does not output glyph metadata. 53 | 54 | "json" 55 | JSON file containing character mapping and metrics. 56 | 57 | "json-full" 58 | JSON file containing character mapping, metrics, and full geometry data. 59 | 60 | "json-simplified" 61 | JSON file as above, but with simplified geometry data where curves have been 62 | converted to line segments, using the '--curve-precision' option. 63 | 64 | -o / --out (default "./glyph_[glyphid]") 65 | Shortcut for both '--img-out' and '--data-out'. 66 | The output filename of each glyph extracted, without the file extension. 67 | You can include the following tags, which will automatically be replaced 68 | by their respective values: 69 | 70 | "[glyphid]" Decimal glyph ID of the current glyph, without the # prefix. 71 | "[unicode]" Hex Unicode codepoint of the current glyph, without the U+ prefix. 72 | 73 | --img-out (default "./glyph_[glyphid]") 74 | The output filename of image files, without the file extension. 75 | You can include the same tags as described above in the '--out' option. 76 | 77 | --data-out (default "./glyph_[glyphid]") 78 | The output filename of data files, without the file extension. 79 | You can include the same tags as described above in the '--out' option. 80 | 81 | --size (default 256) 82 | The size of 1 font unit in pixels (usually the height of a line of text) 83 | for image generation. 84 | 85 | --outline-min 86 | --outline-max 87 | Render glyphs as an outline. 88 | 89 | --sdf-min 90 | --sdf-max 91 | Distance range for signed distance fields. 92 | 93 | --coallesce-unicode 94 | For glyphs that map to many Unicode codepoints, export only one entry under 95 | the most common codepoint, but specify all codepoints in their data files. 96 | 97 | --use-alpha 98 | Use the alpha channel in generated images, instead of the color channels. 99 | 100 | --gamma (default 2.2) 101 | The gamma correction value for grayscale image output. 102 | 103 | --curve-precision (default 100) 104 | The number of line segments to which curves will be converted 105 | for rendering. 106 | 107 | --ignore-img-metrics 108 | Force use normalized EM units in the data output, disregarding any rendered images. 109 | 110 | --ignore-existing 111 | Skips over glyphs which already have a corresponding file in the output location. 112 | ` 113 | 114 | const exitWithUsage = () => 115 | { 116 | console.error(usage) 117 | process.exit(0) 118 | } 119 | 120 | // Parse command line arguments. 121 | const opts = minimist(process.argv.slice(2)) 122 | 123 | if (opts._.length != 1) 124 | exitWithUsage() 125 | 126 | const argFontFile = opts._[0] 127 | const argOut = opts["out"] || opts["o"] || "./glyph_[glyphid]" 128 | const argImgMode = opts["img-mode"] || "png-grayscale" 129 | const argImgOut = opts["img-out"] || argOut 130 | const argSize = parseInt(opts.size) || 256 131 | const argDataMode = opts["data-mode"] || "json" 132 | const argDataOut = opts["data-out"] || argOut 133 | const argCoallesceUnicode = !!opts["coallesce-unicode"] 134 | const argUseAlpha = !!opts["use-alpha"] 135 | const argGamma = parseFloat(opts.gamma) || 2.2 136 | const argCurvePrecision = parseInt(opts["curve-precision"]) || 100 137 | const argIgnoreImgMetrics = !!opts["ignore-img-metrics"] 138 | const argIgnoreExisting = !!opts["ignore-existing"] 139 | 140 | const argSDFMin = parseFloat(opts["sdf-min"]) 141 | const argSDFMax = parseFloat(opts["sdf-max"]) 142 | const argOutlineMin = parseFloat(opts["outline-min"]) 143 | const argOutlineMax = parseFloat(opts["outline-max"]) 144 | 145 | // Load the font file. 146 | const bytes = fs.readFileSync(argFontFile) 147 | const fontCollection = FontCollection.fromBytes(bytes) 148 | const font = fontCollection.fonts[0] 149 | 150 | // Load the glyph list. 151 | const unicodeMap = font.getUnicodeMap() 152 | 153 | const argGlyphs = (opts.glyphs || "*").toLowerCase() 154 | let argGlyphList = parseGlyphRange(argGlyphs) 155 | if (argGlyphs == "*") 156 | { 157 | for (const id of font.enumerateGlyphIds()) 158 | argGlyphList.glyphIds.push(id) 159 | } 160 | else if (argGlyphs == "u+*") 161 | { 162 | for (const [codepoint, glyphId] of unicodeMap) 163 | argGlyphList.unicodeCodepoints.push(codepoint) 164 | } 165 | 166 | for (const unicodeCodepoint of argGlyphList.unicodeCodepoints) 167 | { 168 | if (unicodeMap.has(unicodeCodepoint)) 169 | argGlyphList.glyphIds.push(unicodeMap.get(unicodeCodepoint)) 170 | } 171 | 172 | argGlyphList.glyphIds = [...new Set(argGlyphList.glyphIds)] 173 | 174 | let resolvedGlyphList = [] 175 | for (const glyphId of argGlyphList.glyphIds) 176 | { 177 | if (argCoallesceUnicode) 178 | { 179 | let unicodeCodepoints = [] 180 | for (const [codepoint, id] of unicodeMap) 181 | { 182 | if (glyphId == id) 183 | unicodeCodepoints.push(codepoint) 184 | } 185 | 186 | resolvedGlyphList.push( 187 | { 188 | glyphId, 189 | unicodeCodepoints 190 | }) 191 | } 192 | else 193 | { 194 | let hadACodepoint = false 195 | for (const [codepoint, id] of unicodeMap) 196 | { 197 | if (glyphId == id) 198 | { 199 | hadACodepoint = true 200 | resolvedGlyphList.push( 201 | { 202 | glyphId, 203 | unicodeCodepoints: [codepoint] 204 | }) 205 | } 206 | } 207 | 208 | if (!hadACodepoint) 209 | resolvedGlyphList.push( 210 | { 211 | glyphId, 212 | unicodeCodepoints: [] 213 | }) 214 | } 215 | } 216 | 217 | // Render glyphs. 218 | for (const glyph of resolvedGlyphList) 219 | { 220 | const mainUnicodeCodepoint = (glyph.unicodeCodepoints.length == 0 ? null : glyph.unicodeCodepoints[0]) 221 | 222 | const outputImgFilename = argImgOut 223 | .replace(/\[glyphid\]/g, glyph.glyphId.toString()) 224 | .replace(/\[unicode\]/g, (glyph.unicodeCodepoints.length == 0 ? "" : glyph.unicodeCodepoints[0].toString(16))) 225 | 226 | const outputDataFilename = argDataOut 227 | .replace(/\[glyphid\]/g, glyph.glyphId.toString()) 228 | .replace(/\[unicode\]/g, mainUnicodeCodepoint == null ? "" : mainUnicodeCodepoint.toString(16)) 229 | 230 | let shouldSkip = false 231 | if (argIgnoreExisting) 232 | { 233 | if (argImgMode != "none" && fs.existsSync(outputImgFilename)) 234 | shouldSkip = true 235 | 236 | if (argDataMode == "json" && fs.existsSync(outputDataFilename + ".json")) 237 | shouldSkip = true 238 | 239 | if (argDataMode == "xml-sprsheet" && fs.existsSync(outputDataFilename + ".sprsheet")) 240 | shouldSkip = true 241 | } 242 | 243 | console.log((shouldSkip ? "skipping" : "extracting") + " glyph #" + glyph.glyphId + ": [" + glyph.unicodeCodepoints.map(c => "U+" + c.toString(16)).join(",") + "]...") 244 | if (shouldSkip) 245 | continue 246 | 247 | let renderedImage = null 248 | if (argImgMode != "none") 249 | { 250 | const geometry = font.getGlyphGeometry(glyph.glyphId, argCurvePrecision) 251 | 252 | renderedImage = GlyphRenderer.render(geometry, argSize * 16) 253 | 254 | if (argImgMode == "png-sdf") 255 | { 256 | renderedImage = renderedImage.getWithBorder(argSDFMax * 16) 257 | renderedImage = renderedImage.getSignedDistanceField() 258 | renderedImage.normalizeSignedDistance(argSDFMin * 16, argSDFMax * 16) 259 | } 260 | 261 | else if (!isNaN(argOutlineMin) && !isNaN(argOutlineMax)) 262 | { 263 | renderedImage = renderedImage.getWithBorder(argOutlineMax * 16) 264 | renderedImage = renderedImage.getSignedDistanceField() 265 | renderedImage.outline(argOutlineMin * 16, argOutlineMax * 16) 266 | } 267 | 268 | renderedImage = renderedImage.getDownsampled(argImgMode == "png-sdf" ? 1 : argGamma) 269 | 270 | if (argImgMode == "png-binary") 271 | renderedImage.binarize() 272 | 273 | renderedImage.normalizeColorRange() 274 | 275 | let png = new PNG.PNG({ width: renderedImage.width, height: renderedImage.height, colorType: 6 }) 276 | for (let y = 0; y < renderedImage.height; y++) 277 | { 278 | for (let x = 0; x < renderedImage.width; x++) 279 | { 280 | const i = (y * renderedImage.width + x) 281 | 282 | if (argUseAlpha) 283 | { 284 | png.data[i * 4 + 0] = 255 285 | png.data[i * 4 + 1] = 255 286 | png.data[i * 4 + 2] = 255 287 | png.data[i * 4 + 3] = renderedImage.buffer[i] 288 | } 289 | else 290 | { 291 | png.data[i * 4 + 0] = renderedImage.buffer[i] 292 | png.data[i * 4 + 1] = renderedImage.buffer[i] 293 | png.data[i * 4 + 2] = renderedImage.buffer[i] 294 | png.data[i * 4 + 3] = 255 295 | } 296 | } 297 | } 298 | 299 | fs.writeFileSync(outputImgFilename + ".png", PNG.PNG.sync.write(png)) 300 | } 301 | 302 | if (argDataMode != "none") 303 | { 304 | const geometry = font.getGlyphGeometry(glyph.glyphId, argDataMode != "json-simplified" ? 0 : argCurvePrecision) 305 | geometry.xMin = geometry.xMin || 0 306 | geometry.xMax = geometry.xMax || 0 307 | geometry.yMin = geometry.yMin || 0 308 | geometry.yMax = geometry.yMax || 0 309 | geometry.advance = geometry.advance || 0 310 | 311 | let metrics = 312 | { 313 | width: geometry.xMax - geometry.xMin, 314 | height: geometry.yMax - geometry.yMin, 315 | xOrigin: 0, 316 | yOrigin: 0, 317 | xAdvance: geometry.advance, 318 | emToPixels: null, 319 | } 320 | 321 | if (renderedImage && !argIgnoreImgMetrics) 322 | { 323 | metrics = 324 | { 325 | width: renderedImage.width, 326 | height: renderedImage.height, 327 | xOrigin: renderedImage.xOrigin, 328 | yOrigin: renderedImage.yOrigin, 329 | xAdvance: renderedImage.emScale * geometry.advance, 330 | emToPixels: renderedImage.emScale, 331 | } 332 | } 333 | 334 | let json = 335 | { 336 | ...glyph, 337 | ...metrics, 338 | } 339 | 340 | if (argDataMode != "json") 341 | json.contours = geometry.contours 342 | 343 | if (argDataMode != "xml-sprsheet") 344 | { 345 | const jsonStr = JSON.stringify(json, null, 4) 346 | fs.writeFileSync(outputDataFilename + ".json", jsonStr) 347 | } 348 | else 349 | { 350 | const filenameWithoutFolder = path.basename(outputDataFilename + ".png") 351 | const xml = 352 | ` 353 | 354 | 355 | 356 | 357 | ` 358 | 359 | fs.writeFileSync(outputDataFilename + ".sprsheet", xml) 360 | } 361 | } 362 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Font Inspect 8 | 9 | 10 | 11 | 65 | 66 | 67 | 68 |
69 |
70 | 71 | Check the Console for extra output! 72 |
73 |
74 | Sub-font: 75 | 76 | 77 | 78 |
79 | (Double-click a glyph to print geometry data to the Console and inspect its Unicode codepoint.) 80 |
81 |
82 |
83 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { FontCollection, Font } from "./src/font.js" 2 | import { GlyphRenderer } from "./src/renderer.js" 3 | 4 | 5 | export { FontCollection, Font, GlyphRenderer } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hlorenzi/font", 3 | "version": "1.2.0", 4 | "description": "Utilities and a CLI for extracting glyphs and metadata from font files.", 5 | "repository": "github:hlorenzi/font-js", 6 | "homepage": "https://github.com/hlorenzi/font-js#readme", 7 | "bugs": "https://github.com/hlorenzi/font-js/issues", 8 | "keywords": [ 9 | "font", 10 | "glyph", 11 | "extract", 12 | "ttf", 13 | "otf", 14 | "cli" 15 | ], 16 | "type": "module", 17 | "main": "./index.js", 18 | "scripts": { 19 | "start": "node cli.js", 20 | "build": "webpack", 21 | "watch": "webpack --watch --mode development", 22 | "test": "node test.js" 23 | }, 24 | "author": "hlorenzi", 25 | "license": "ISC", 26 | "bin": { 27 | "font": "cli.js" 28 | }, 29 | "dependencies": { 30 | "@hlorenzi/buffer": "^1.2.0", 31 | "minimist": "^1.2.0", 32 | "pngjs": "^3.3.3" 33 | }, 34 | "devDependencies": { 35 | "webpack": "^4.36.1", 36 | "webpack-cli": "^3.3.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /run_local_server.bat: -------------------------------------------------------------------------------- 1 | where /q http-server 2 | 3 | IF ERRORLEVEL 1 (npm i -g http-server) 4 | 5 | http-server -p 80 -------------------------------------------------------------------------------- /src/font.js: -------------------------------------------------------------------------------- 1 | import { BufferReader } from "@hlorenzi/buffer" 2 | 3 | 4 | export class FontCollection 5 | { 6 | constructor() 7 | { 8 | this.warnings = [] 9 | this.fonts = [] 10 | } 11 | 12 | 13 | static fromBytes(bytes, preparseGlyphs = false) 14 | { 15 | return FontCollection.fromReader(new BufferReader(bytes), preparseGlyphs) 16 | } 17 | 18 | 19 | static fromReader(r, preparseGlyphs = false) 20 | { 21 | let fontCollection = new FontCollection() 22 | fontCollection.data = r 23 | 24 | fontCollection.readCollectionHeader(r) 25 | 26 | for (let offset of fontCollection.offsetTables) 27 | { 28 | const rCloned = new BufferReader(r.bytes) 29 | rCloned.seek(offset) 30 | 31 | fontCollection.fonts.push(Font.fromReader(rCloned, preparseGlyphs)) 32 | } 33 | 34 | return fontCollection 35 | } 36 | 37 | 38 | getFont(index) 39 | { 40 | return this.fonts[index] 41 | } 42 | 43 | 44 | readCollectionHeader(r) 45 | { 46 | const tag = r.readAsciiLength(4) 47 | 48 | if (tag == "ttcf") 49 | { 50 | this.ttcTag = tag 51 | this.majorVersion = r.readUint16BE() 52 | this.minorVersion = r.readUint16BE() 53 | this.numFonts = r.readUint32BE() 54 | this.offsetTables = r.readManyUint32BE(this.numFonts) 55 | 56 | if (this.majorVersion == 2) 57 | { 58 | this.dsigTag = r.readUint32BE() 59 | this.dsigLength = r.readUint32BE() 60 | this.dsigOffset = r.readUint32BE() 61 | } 62 | } 63 | else 64 | { 65 | this.offsetTables = [0] 66 | } 67 | } 68 | } 69 | 70 | 71 | export class Font 72 | { 73 | constructor() 74 | { 75 | this.warnings = [] 76 | } 77 | 78 | 79 | static fromBytes(bytes, preparseGlyphs = false) 80 | { 81 | return Font.fromReader(new BufferReader(bytes), preparseGlyphs) 82 | } 83 | 84 | 85 | static fromReader(r, preparseGlyphs = false) 86 | { 87 | let font = new Font() 88 | font.data = r 89 | 90 | font.readOffsetTable(r) 91 | font.readTableRecord(r) 92 | font.readFontHeaderTable(r) 93 | font.readNamingTable(r) 94 | font.readHorizontalHeaderTable(r) 95 | font.readMaximumProfileTable(r) 96 | font.readHorizontalMetricsTable(r) 97 | font.readIndexToLocationTable(r) 98 | 99 | if (preparseGlyphs) 100 | font.readGlyphDataTable(r) 101 | 102 | font.readCharacterToGlyphIndexMappingTable(r) 103 | return font 104 | } 105 | 106 | 107 | *enumerateGlyphIds() 108 | { 109 | const count = this.getGlyphCount() 110 | 111 | for (let i = 0; i < count; i++) 112 | yield i 113 | } 114 | 115 | 116 | getGlyphCount() 117 | { 118 | const maxpTable = this.getTable("maxp") 119 | 120 | return maxpTable.numGlyphs 121 | } 122 | 123 | 124 | getHorizontalLineMetrics() 125 | { 126 | const headTable = this.getTable("head") 127 | const hheaTable = this.getTable("hhea") 128 | 129 | return { 130 | lineTop: -hheaTable.ascender / headTable.unitsPerEm, 131 | lineBottom: -hheaTable.descender / headTable.unitsPerEm, 132 | lineGap: hheaTable.lineGap / headTable.unitsPerEm 133 | } 134 | } 135 | 136 | 137 | getGlyphIndexForUnicode(unicode) 138 | { 139 | return this.getUnicodeMap().get(unicode) 140 | } 141 | 142 | 143 | getUnicodeMap() 144 | { 145 | const cmapTable = this.getTable("cmap") 146 | 147 | return cmapTable.unicodeToGlyphMap 148 | } 149 | 150 | 151 | warnIf(condition, message) 152 | { 153 | if (condition) 154 | this.warnings.push(message) 155 | } 156 | 157 | 158 | getTable(tag) 159 | { 160 | const table = this.tables.find(t => t.tableTag == tag) 161 | if (table == null) 162 | throw "missing table `" + tag + "`" 163 | 164 | return table 165 | } 166 | 167 | 168 | get usesTrueTypeOutlines() 169 | { 170 | return this.sfntVersion != "OTTO" 171 | } 172 | 173 | 174 | readOffsetTable(r) 175 | { 176 | this.sfntVersion = r.readAsciiLength(4) 177 | this.numTables = r.readUint16BE() 178 | this.searchRange = r.readUint16BE() 179 | this.entrySelector = r.readUint16BE() 180 | this.rangeShift = r.readUint16BE() 181 | } 182 | 183 | 184 | readTableRecord(r) 185 | { 186 | this.tables = [] 187 | 188 | for (let i = 0; i < this.numTables; i++) 189 | { 190 | let table = { } 191 | table.tableTag = r.readAsciiLength(4) 192 | table.checkSum = r.readUint32BE() 193 | table.offset = r.readUint32BE() 194 | table.length = r.readUint32BE() 195 | 196 | this.tables.push(table) 197 | } 198 | } 199 | 200 | 201 | readFontHeaderTable(r) 202 | { 203 | let table = this.getTable("head") 204 | 205 | r.seek(table.offset) 206 | 207 | table.majorVersion = r.readUint16BE() 208 | table.minorVersion = r.readUint16BE() 209 | table.fontRevision = r.readUint32BE() 210 | 211 | table.checkSumAdjustment = r.readUint32BE() 212 | table.magicNumber = r.readUint32BE() 213 | table.flags = r.readUint16BE() 214 | table.unitsPerEm = r.readUint16BE() 215 | 216 | table.createdHi = r.readUint32BE() 217 | table.createdLo = r.readUint32BE() 218 | table.modifiedHi = r.readUint32BE() 219 | table.modifiedLo = r.readUint32BE() 220 | 221 | table.xMin = r.readInt16BE() 222 | table.yMin = r.readInt16BE() 223 | table.xMax = r.readInt16BE() 224 | table.yMax = r.readInt16BE() 225 | 226 | table.macStyle = r.readUint16BE() 227 | table.lowestRecPPEM = r.readUint16BE() 228 | table.fontDirectionHint = r.readInt16BE() 229 | table.indexToLocFormat = r.readInt16BE() 230 | table.glyphDataFormat = r.readInt16BE() 231 | 232 | if (table.indexToLocFormat != 0 && table.indexToLocFormat != 1) 233 | throw "invalid `head` indexToLocFormat" 234 | } 235 | 236 | 237 | readNamingTable(r) 238 | { 239 | let table = this.getTable("name") 240 | 241 | r.seek(table.offset) 242 | 243 | table.format = r.readUint16BE() 244 | table.count = r.readUint16BE() 245 | table.stringOffset = r.readUint16BE() 246 | 247 | table.nameRecords = [] 248 | for (let i = 0; i < table.count; i++) 249 | { 250 | let nameRecord = { } 251 | nameRecord.platformID = r.readUint16BE() 252 | nameRecord.encodingID = r.readUint16BE() 253 | nameRecord.languageID = r.readUint16BE() 254 | nameRecord.nameID = r.readUint16BE() 255 | nameRecord.length = r.readUint16BE() 256 | nameRecord.offset = r.readUint16BE() 257 | nameRecord.string = null 258 | table.nameRecords.push(nameRecord) 259 | } 260 | 261 | if (table.format == 1) 262 | { 263 | table.langTagCount = r.readUint16BE() 264 | 265 | table.langTagRecords = [] 266 | for (let i = 0; i < table.langTagCount; i++) 267 | { 268 | let langTagRecord = { } 269 | langTagRecord.length = r.readUint16BE() 270 | langTagRecord.offset = r.readUint16BE() 271 | langTagRecord.string = null 272 | table.langTagRecords.push(langTagRecord) 273 | } 274 | } 275 | 276 | const stringDataOffset = r.head 277 | 278 | for (let nameRecord of table.nameRecords) 279 | { 280 | if ((nameRecord.platformID == 0) || 281 | (nameRecord.platformID == 3 && nameRecord.encodingID == 1)) 282 | { 283 | r.seek(stringDataOffset + nameRecord.offset) 284 | nameRecord.string = r.readUtf16BELength(nameRecord.length / 2) 285 | } 286 | } 287 | 288 | const findMostSuitableString = (nameID) => 289 | { 290 | let foundString = null 291 | for (let nameRecord of table.nameRecords) 292 | { 293 | if (nameRecord.string == null) 294 | continue 295 | 296 | if (nameRecord.nameID != nameID) 297 | continue 298 | 299 | if (nameRecord.languageID == 0) 300 | return nameRecord.string 301 | 302 | foundString = nameRecord.string 303 | } 304 | 305 | return foundString 306 | } 307 | 308 | this.fontCopyright = findMostSuitableString(0) 309 | this.fontFamilyName = findMostSuitableString(1) 310 | this.fontSubfamilyName = findMostSuitableString(2) 311 | this.fontUniqueIdentifier = findMostSuitableString(3) 312 | this.fontFullName = findMostSuitableString(4) 313 | this.fontVersionString = findMostSuitableString(5) 314 | this.fontPostScriptName = findMostSuitableString(6) 315 | this.fontTrademark = findMostSuitableString(7) 316 | this.fontManufacturer = findMostSuitableString(8) 317 | } 318 | 319 | 320 | readHorizontalHeaderTable(r) 321 | { 322 | let table = this.getTable("hhea") 323 | 324 | r.seek(table.offset) 325 | 326 | table.majorVersion = r.readUint16BE() 327 | table.minorVersion = r.readUint16BE() 328 | 329 | table.ascender = r.readInt16BE() 330 | table.descender = r.readInt16BE() 331 | table.lineGap = r.readInt16BE() 332 | 333 | table.advanceWidthMax = r.readUint16BE() 334 | 335 | table.minLeftSideBearing = r.readInt16BE() 336 | table.minRightSideBearing = r.readInt16BE() 337 | 338 | table.xMaxExtent = r.readInt16BE() 339 | 340 | table.caretSlopeRise = r.readInt16BE() 341 | table.caretSlopeRun = r.readInt16BE() 342 | table.caretOffset = r.readInt16BE() 343 | 344 | table.reserved0 = r.readInt16BE() 345 | table.reserved1 = r.readInt16BE() 346 | table.reserved2 = r.readInt16BE() 347 | table.reserved3 = r.readInt16BE() 348 | 349 | table.metricDataFormat = r.readInt16BE() 350 | 351 | table.numberOfHMetrics = r.readUint16BE() 352 | } 353 | 354 | 355 | readHorizontalMetricsTable(r) 356 | { 357 | const hhea = this.getTable("hhea") 358 | const maxp = this.getTable("maxp") 359 | let hmtx = this.getTable("hmtx") 360 | 361 | r.seek(hmtx.offset) 362 | 363 | hmtx.hMetrics = [] 364 | for (let i = 0; i < hhea.numberOfHMetrics; i++) 365 | { 366 | let hMetric = { } 367 | hMetric.advanceWidth = r.readUint16BE() 368 | hMetric.lsb = r.readInt16BE() 369 | 370 | hmtx.hMetrics.push(hMetric) 371 | } 372 | 373 | hmtx.leftSideBearings = r.readManyInt16BE(maxp.numGlyphs - hhea.numberOfHMetrics) 374 | } 375 | 376 | 377 | readMaximumProfileTable(r) 378 | { 379 | let table = this.getTable("maxp") 380 | 381 | r.seek(table.offset) 382 | 383 | if (!this.usesTrueTypeOutlines) 384 | { 385 | table.version = r.readUint32BE() 386 | this.warnIf(table.version != 0x5000, "invalid `maxp` version") 387 | 388 | table.numGlyphs = r.readUint16BE() 389 | } 390 | else 391 | { 392 | table.version = r.readUint32BE() 393 | this.warnIf(table.version != 0x10000, "invalid `maxp` version") 394 | 395 | table.numGlyphs = r.readUint16BE() 396 | table.maxPoints = r.readUint16BE() 397 | table.maxContours = r.readUint16BE() 398 | table.maxCompositePoints = r.readUint16BE() 399 | table.maxCompositeContours = r.readUint16BE() 400 | table.maxZones = r.readUint16BE() 401 | table.maxTwilightPoints = r.readUint16BE() 402 | table.maxStorage = r.readUint16BE() 403 | table.maxFunctionDefs = r.readUint16BE() 404 | table.maxInstructionDefs = r.readUint16BE() 405 | table.maxStackElements = r.readUint16BE() 406 | table.maxSizeOfInstructions = r.readUint16BE() 407 | table.maxComponentElements = r.readUint16BE() 408 | table.maxComponentDepth = r.readUint16BE() 409 | } 410 | } 411 | 412 | 413 | readIndexToLocationTable(r) 414 | { 415 | const headTable = this.getTable("head") 416 | const maxpTable = this.getTable("maxp") 417 | let locaTable = this.getTable("loca") 418 | 419 | const locaEntryNum = maxpTable.numGlyphs + 1 420 | 421 | r.seek(locaTable.offset) 422 | 423 | if (headTable.indexToLocFormat == 0) 424 | locaTable.offsets = r.readManyUint16BE(locaEntryNum).map(i => i * 2) 425 | 426 | else if (headTable.indexToLocFormat == 1) 427 | locaTable.offsets = r.readManyUint32BE(locaEntryNum) 428 | 429 | for (let i = 1; i < locaTable.offsets.length; i++) 430 | { 431 | if (locaTable.offsets[i] < locaTable.offsets[i - 1]) 432 | throw "invalid `loca` offsets entry" 433 | } 434 | 435 | for (let i = 0; i < locaTable.offsets.length - 1; i++) 436 | { 437 | if (locaTable.offsets[i] == locaTable.offsets[i + 1]) 438 | locaTable.offsets[i] = null 439 | } 440 | } 441 | 442 | 443 | readGlyphDataTable(r) 444 | { 445 | const maxpTable = this.getTable("maxp") 446 | const locaTable = this.getTable("loca") 447 | let glyfTable = this.getTable("glyf") 448 | 449 | glyfTable.glyphs = [] 450 | 451 | for (let i = 0; i < maxpTable.numGlyphs; i++) 452 | { 453 | if (locaTable.offsets[i] != null) 454 | { 455 | r.seek(glyfTable.offset + locaTable.offsets[i]) 456 | glyfTable.glyphs.push(this.readGlyph(r, i)) 457 | } 458 | else 459 | glyfTable.glyphs.push(null) 460 | } 461 | } 462 | 463 | 464 | readGlyph(r, glyphId) 465 | { 466 | let glyph = { } 467 | 468 | glyph.numberOfContours = r.readInt16BE() 469 | 470 | glyph.xMin = r.readInt16BE() 471 | glyph.yMin = r.readInt16BE() 472 | glyph.xMax = r.readInt16BE() 473 | glyph.yMax = r.readInt16BE() 474 | 475 | if (glyph.numberOfContours >= 0) 476 | this.readGlyphSimple(r, glyph) 477 | 478 | else 479 | this.readGlyphComposite(r, glyph, glyphId) 480 | 481 | return glyph 482 | } 483 | 484 | 485 | readGlyphSimple(r, glyph) 486 | { 487 | glyph.endPtsOfContours = r.readManyUint16BE(glyph.numberOfContours) 488 | for (let i = 1; i < glyph.endPtsOfContours.length; i++) 489 | { 490 | if (glyph.endPtsOfContours[i] < glyph.endPtsOfContours[i - 1]) 491 | throw "invalid glyph endPtsOfContours entry" 492 | } 493 | 494 | glyph.instructionLength = r.readUint16BE() 495 | glyph.instructions = r.readManyUint8(glyph.instructionLength) 496 | 497 | const numPoints = glyph.endPtsOfContours[glyph.numberOfContours - 1] + 1 498 | 499 | const X_SHORT_VECTOR_FLAG = 0x02 500 | const Y_SHORT_VECTOR_FLAG = 0x04 501 | const REPEAT_FLAG = 0x08 502 | const X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR_FLAG = 0x10 503 | const Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR_FLAG = 0x20 504 | 505 | // Read flags while expanding it to the logical flag array 506 | glyph.flags = [] 507 | 508 | while (glyph.flags.length < numPoints) 509 | { 510 | const flag = r.readUint8() 511 | glyph.flags.push(flag) 512 | 513 | if ((flag & REPEAT_FLAG) != 0) 514 | { 515 | const repeatCount = r.readUint8() 516 | for (let i = 0; i < repeatCount; i++) 517 | glyph.flags.push(flag) 518 | } 519 | } 520 | 521 | // Read X coordinates 522 | glyph.xCoordinates = [] 523 | 524 | let xCurrent = 0 525 | for (let i = 0; i < numPoints; i++) 526 | { 527 | const flag = glyph.flags[i] 528 | 529 | if ((flag & X_SHORT_VECTOR_FLAG) != 0) 530 | { 531 | const displacement = r.readUint8() 532 | const sign = (flag & X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR_FLAG) != 0 ? 1 : -1 533 | xCurrent += displacement * sign 534 | } 535 | 536 | else if ((flag & X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR_FLAG) == 0) 537 | xCurrent += r.readInt16BE() 538 | 539 | glyph.xCoordinates.push(xCurrent) 540 | } 541 | 542 | // Read Y coordinates 543 | glyph.yCoordinates = [] 544 | 545 | let yCurrent = 0 546 | for (let i = 0; i < numPoints; i++) 547 | { 548 | const flag = glyph.flags[i] 549 | 550 | if ((flag & Y_SHORT_VECTOR_FLAG) != 0) 551 | { 552 | const displacement = r.readUint8() 553 | const sign = (flag & Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR_FLAG) != 0 ? 1 : -1 554 | yCurrent += displacement * sign 555 | } 556 | 557 | else if ((flag & Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR_FLAG) == 0) 558 | yCurrent += r.readInt16BE() 559 | 560 | glyph.yCoordinates.push(yCurrent) 561 | } 562 | } 563 | 564 | 565 | readGlyphComposite(r, glyph, glyphId) 566 | { 567 | const ARG_1_AND_2_ARE_WORDS_FLAG = 0x0001 568 | const ARGS_ARE_XY_VALUES_FLAG = 0x0002 569 | const WE_HAVE_A_SCALE_FLAG = 0x0008 570 | const MORE_COMPONENTS_FLAG = 0x0020 571 | const WE_HAVE_AN_X_AND_Y_SCALE_FLAG = 0x0040 572 | const WE_HAVE_A_TWO_BY_TWO_FLAG = 0x0080 573 | const WE_HAVE_INSTRUCTIONS = 0x0100 574 | 575 | glyph.components = [] 576 | 577 | while (true) 578 | { 579 | let component = { } 580 | glyph.components.push(component) 581 | 582 | component.flags = r.readUint16BE() 583 | component.glyphIndex = r.readUint16BE() 584 | 585 | component.xScale = 1 586 | component.yScale = 1 587 | component.scale01 = 0 588 | component.scale10 = 0 589 | 590 | if ((component.flags & ARG_1_AND_2_ARE_WORDS_FLAG) != 0) 591 | { 592 | component.argument1 = r.readInt16BE() 593 | component.argument2 = r.readInt16BE() 594 | } 595 | else 596 | { 597 | component.argument1 = r.readInt8() 598 | component.argument2 = r.readInt8() 599 | } 600 | 601 | if ((component.flags & ARGS_ARE_XY_VALUES_FLAG) == 0) 602 | this.warnings.push("glyph 0x" + glyphId.toString(16) + ": unsupported cleared ARGS_ARE_XY_VALUES flag") 603 | 604 | if ((component.flags & WE_HAVE_A_SCALE_FLAG) != 0) 605 | { 606 | component.xScale = component.yScale = this.readF2Dot14(r) 607 | } 608 | 609 | else if ((component.flags & WE_HAVE_AN_X_AND_Y_SCALE_FLAG) != 0) 610 | { 611 | component.xScale = this.readF2Dot14(r) 612 | component.yScale = this.readF2Dot14(r) 613 | } 614 | 615 | else if ((component.flags & WE_HAVE_A_TWO_BY_TWO_FLAG) != 0) 616 | { 617 | component.xScale = this.readF2Dot14(r) 618 | component.scale01 = this.readF2Dot14(r) 619 | component.scale10 = this.readF2Dot14(r) 620 | component.yScale = this.readF2Dot14(r) 621 | } 622 | 623 | if ((component.flags & MORE_COMPONENTS_FLAG) == 0) 624 | break 625 | } 626 | 627 | // Uses flag of last component? 628 | //if ((component.flags & WE_HAVE_INSTRUCTIONS_FLAG) != 0) 629 | //{ 630 | // 631 | //} 632 | } 633 | 634 | 635 | readCharacterToGlyphIndexMappingTable(r) 636 | { 637 | let cmapTable = this.getTable("cmap") 638 | 639 | r.seek(cmapTable.offset) 640 | 641 | cmapTable.version = r.readUint16BE() 642 | cmapTable.numTables = r.readUint16BE() 643 | 644 | cmapTable.encodingRecords = [] 645 | 646 | for (let i = 0; i < cmapTable.numTables; i++) 647 | { 648 | let encodingRecord = { } 649 | 650 | encodingRecord.platformID = r.readUint16BE() 651 | encodingRecord.encodingID = r.readUint16BE() 652 | encodingRecord.offset = r.readUint32BE() 653 | encodingRecord.subtable = null 654 | 655 | cmapTable.encodingRecords.push(encodingRecord) 656 | } 657 | 658 | for (let encodingRecord of cmapTable.encodingRecords) 659 | { 660 | r.seek(cmapTable.offset + encodingRecord.offset) 661 | 662 | encodingRecord.subtable = { } 663 | encodingRecord.subtable.unicodeToGlyphMap = new Map() 664 | 665 | encodingRecord.subtable.format = r.readUint16BE() 666 | 667 | switch (encodingRecord.subtable.format) 668 | { 669 | case 4: 670 | this.readCharacterToGlyphIndexMappingEncodingSubtableFormat4(r, encodingRecord.subtable) 671 | break 672 | case 12: 673 | this.readCharacterToGlyphIndexMappingEncodingSubtableFormat12(r, encodingRecord.subtable) 674 | break 675 | } 676 | } 677 | 678 | const UNICODE_PLATFORMID = 0 679 | const WINDOWS_PLATFORMID = 3 680 | const WINDOWS_UNICODEBMP_ENCODINGID = 1 681 | const WINDOWS_UNICODEFULL_ENCODINGID = 10 682 | 683 | cmapTable.unicodeToGlyphMap = new Map() 684 | 685 | for (let encodingRecord of cmapTable.encodingRecords) 686 | { 687 | if (encodingRecord.platformID == UNICODE_PLATFORMID || 688 | (encodingRecord.platformID == WINDOWS_PLATFORMID && encodingRecord.encodingID == WINDOWS_UNICODEBMP_ENCODINGID) || 689 | (encodingRecord.platformID == WINDOWS_PLATFORMID && encodingRecord.encodingID == WINDOWS_UNICODEFULL_ENCODINGID)) 690 | { 691 | for (const [code, glyphId] of encodingRecord.subtable.unicodeToGlyphMap) 692 | cmapTable.unicodeToGlyphMap.set(code, glyphId) 693 | } 694 | } 695 | } 696 | 697 | 698 | readCharacterToGlyphIndexMappingEncodingSubtableFormat4(r, subtable) 699 | { 700 | subtable.length = r.readUint16BE() 701 | subtable.language = r.readUint16BE() 702 | subtable.segCountX2 = r.readUint16BE() 703 | subtable.searchRange = r.readUint16BE() 704 | subtable.entrySelector = r.readUint16BE() 705 | subtable.rangeShift = r.readUint16BE() 706 | 707 | subtable.endCode = r.readManyUint16BE(subtable.segCountX2 / 2) 708 | 709 | subtable.reservedPad = r.readUint16BE() 710 | 711 | subtable.startCode = r.readManyUint16BE(subtable.segCountX2 / 2) 712 | subtable.idDelta = r.readManyInt16BE(subtable.segCountX2 / 2) 713 | 714 | const idRangeOffsetPosition = r.head 715 | subtable.idRangeOffset = r.readManyUint16BE(subtable.segCountX2 / 2) 716 | 717 | for (let c = 0; c <= 0xffff; c++) 718 | { 719 | for (let i = 0; i < subtable.segCountX2 / 2; i++) 720 | { 721 | if (c > subtable.endCode[i]) 722 | continue 723 | 724 | if (c < subtable.startCode[i]) 725 | continue 726 | 727 | if (subtable.idRangeOffset[i] == 0) 728 | { 729 | const glyphId = (c + subtable.idDelta[i]) % 0x10000 730 | 731 | subtable.unicodeToGlyphMap.set(c, glyphId) 732 | } 733 | else 734 | { 735 | const addr = 736 | ((subtable.idRangeOffset[i] / 2) + (c - subtable.startCode[i])) * 2 + 737 | (idRangeOffsetPosition + i * 2) 738 | 739 | r.seek(addr) 740 | 741 | let glyphId = r.readUint16BE() 742 | if (glyphId != 0) 743 | glyphId = (glyphId + subtable.idDelta[i]) % 0x10000 744 | 745 | subtable.unicodeToGlyphMap.set(c, glyphId) 746 | } 747 | 748 | break 749 | } 750 | } 751 | } 752 | 753 | 754 | readCharacterToGlyphIndexMappingEncodingSubtableFormat12(r, subtable) 755 | { 756 | subtable.reserved = r.readUint16BE() 757 | subtable.length = r.readUint32BE() 758 | subtable.language = r.readUint32BE() 759 | subtable.numGroups = r.readUint32BE() 760 | 761 | subtable.groups = [] 762 | for (let i = 0; i < subtable.numGroups; i++) 763 | { 764 | let group = { } 765 | group.startCharCode = r.readUint32BE() 766 | group.endCharCode = r.readUint32BE() 767 | group.startGlyphID = r.readUint32BE() 768 | 769 | subtable.groups.push(group) 770 | } 771 | 772 | for (const group of subtable.groups) 773 | { 774 | for (let c = group.startCharCode; c <= group.endCharCode; c++) 775 | subtable.unicodeToGlyphMap.set(c, group.startGlyphID + c - group.startCharCode) 776 | } 777 | } 778 | 779 | 780 | readF2Dot14(r) 781 | { 782 | const raw = r.readUint16BE() 783 | 784 | const rawIntegerPart = (raw & 0xc000) >> 14 785 | const rawFractionalPart = (raw & 0x3fff) 786 | 787 | const integerPart = (rawIntegerPart & 0x2) ? (2 - rawIntegerPart) : rawIntegerPart 788 | const fractionalPart = rawFractionalPart / 16384 789 | 790 | return integerPart + fractionalPart 791 | } 792 | 793 | 794 | fetchGlyphOnDemand(r, glyphId) 795 | { 796 | const maxpTable = this.getTable("maxp") 797 | const locaTable = this.getTable("loca") 798 | const glyfTable = this.getTable("glyf") 799 | 800 | if (glyphId < 0 || glyphId > maxpTable.numGlyphs) 801 | return null 802 | 803 | if (locaTable.offsets[glyphId] == null) 804 | return null 805 | 806 | r.seek(glyfTable.offset + locaTable.offsets[glyphId]) 807 | return this.readGlyph(r, glyphId) 808 | } 809 | 810 | 811 | getGlyphData(glyphId) 812 | { 813 | const glyfTable = this.getTable("glyf") 814 | 815 | return (glyfTable.glyphs ? glyfTable.glyphs[glyphId] : this.fetchGlyphOnDemand(this.data, glyphId)) 816 | } 817 | 818 | 819 | getGlyphGeometry(glyphId, simplifySteps = 0) 820 | { 821 | const headTable = this.getTable("head") 822 | const hmtxTable = this.getTable("hmtx") 823 | const glyph = this.getGlyphData(glyphId) 824 | 825 | const ON_CURVE_POINT_FLAG = 0x01 826 | 827 | const scale = 1 / headTable.unitsPerEm 828 | 829 | let geometry = { } 830 | geometry.contours = [] 831 | geometry.xMin = null 832 | geometry.yMin = null 833 | geometry.xMax = null 834 | geometry.yMax = null 835 | 836 | let hMetric = null 837 | if (glyphId >= hmtxTable.hMetrics.length) 838 | hMetric = hmtxTable.hMetrics[hmtxTable.hMetrics.length - 1] 839 | else 840 | hMetric = hmtxTable.hMetrics[glyphId] 841 | 842 | geometry.advance = (hMetric ? hMetric.advanceWidth / headTable.unitsPerEm : 0) 843 | 844 | const updateMeasures = (x, y) => 845 | { 846 | geometry.xMin = geometry.xMin == null ? x : Math.min(geometry.xMin, x) 847 | geometry.xMax = geometry.xMax == null ? x : Math.max(geometry.xMax, x) 848 | geometry.yMin = geometry.yMin == null ? y : Math.min(geometry.yMin, y) 849 | geometry.yMax = geometry.yMax == null ? y : Math.max(geometry.yMax, y) 850 | } 851 | 852 | if (glyph != null) 853 | { 854 | geometry.isComposite = (glyph.numberOfContours <= 0) 855 | 856 | // Simple glyph 857 | if (!geometry.isComposite) 858 | { 859 | for (let i = 0; i < glyph.endPtsOfContours.length; i++) 860 | { 861 | const firstPoint = (i == 0 ? 0 : glyph.endPtsOfContours[i - 1] + 1) 862 | const lastPoint = glyph.endPtsOfContours[i] 863 | 864 | let segments = [] 865 | 866 | const pointsInContour = (lastPoint + 1 - firstPoint) 867 | 868 | for (let p = firstPoint; p <= lastPoint; p++) 869 | { 870 | const pNext = (p - firstPoint + 1) % pointsInContour + firstPoint 871 | const pPrev = (p - firstPoint - 1 + pointsInContour) % pointsInContour + firstPoint 872 | 873 | let x = scale * glyph.xCoordinates[p] 874 | let y = -scale * glyph.yCoordinates[p] 875 | let xNext = scale * glyph.xCoordinates[pNext] 876 | let yNext = -scale * glyph.yCoordinates[pNext] 877 | let xPrev = scale * glyph.xCoordinates[pPrev] 878 | let yPrev = -scale * glyph.yCoordinates[pPrev] 879 | 880 | if ((glyph.flags[p] & ON_CURVE_POINT_FLAG) == 0) 881 | { 882 | if ((glyph.flags[pPrev] & ON_CURVE_POINT_FLAG) == 0) 883 | { 884 | xPrev = (xPrev + x) / 2 885 | yPrev = (yPrev + y) / 2 886 | } 887 | 888 | if ((glyph.flags[pNext] & ON_CURVE_POINT_FLAG) == 0) 889 | { 890 | xNext = (xNext + x) / 2 891 | yNext = (yNext + y) / 2 892 | } 893 | 894 | segments.push({ 895 | kind: "qbezier", 896 | x1: xPrev, y1: yPrev, 897 | x2: x, y2: y, 898 | x3: xNext, y3: yNext 899 | }) 900 | } 901 | else if ((glyph.flags[pNext] & ON_CURVE_POINT_FLAG) != 0) 902 | { 903 | segments.push({ 904 | kind: "line", 905 | x1: x, y1: y, 906 | x2: xNext, y2: yNext 907 | }) 908 | } 909 | } 910 | 911 | geometry.contours.push(segments) 912 | } 913 | } 914 | 915 | // Composite glyph 916 | else 917 | { 918 | const ARGS_ARE_XY_VALUES_FLAG = 0x0002 919 | 920 | for (const component of glyph.components) 921 | { 922 | const componentGeometry = this.getGlyphGeometry(component.glyphIndex) 923 | if (componentGeometry == null) 924 | continue 925 | 926 | if ((component.flags & ARGS_ARE_XY_VALUES_FLAG) == 0) 927 | return null 928 | 929 | const xOffset = scale * component.argument1 930 | const yOffset = -scale * component.argument2 931 | 932 | for (const contour of componentGeometry.contours) 933 | { 934 | for (const segment of contour) 935 | { 936 | const newX1 = (segment.x1 * component.xScale) + (segment.y1 * component.scale01) + xOffset 937 | const newY1 = (segment.y1 * component.yScale) + (segment.x1 * component.scale10) + yOffset 938 | const newX2 = (segment.x2 * component.xScale) + (segment.y2 * component.scale01) + xOffset 939 | const newY2 = (segment.y2 * component.yScale) + (segment.x2 * component.scale10) + yOffset 940 | segment.x1 = newX1 941 | segment.y1 = newY1 942 | segment.x2 = newX2 943 | segment.y2 = newY2 944 | 945 | if (segment.kind == "qbezier") 946 | { 947 | const newX3 = (segment.x3 * component.xScale) + (segment.y3 * component.scale01) + xOffset 948 | const newY3 = (segment.y3 * component.yScale) + (segment.x3 * component.scale10) + yOffset 949 | segment.x3 = newX3 950 | segment.y3 = newY3 951 | } 952 | } 953 | 954 | geometry.contours.push(contour) 955 | } 956 | } 957 | } 958 | 959 | for (const contour of geometry.contours) 960 | { 961 | for (const segment of contour) 962 | { 963 | updateMeasures(segment.x1, segment.y1) 964 | updateMeasures(segment.x2, segment.y2) 965 | 966 | if (segment.kind == "qbezier") 967 | updateMeasures(segment.x3, segment.y3) 968 | } 969 | } 970 | } 971 | 972 | if (simplifySteps > 0) 973 | geometry = Font.simplifyBezierContours(geometry, simplifySteps) 974 | 975 | return geometry 976 | } 977 | 978 | 979 | static simplifyBezierContours(geometry, steps = 100) 980 | { 981 | if (geometry == null) 982 | return geometry 983 | 984 | let simplifiedContours = [] 985 | 986 | for (const contour of geometry.contours) 987 | { 988 | let simplifiedSegments = [] 989 | 990 | for (const segment of contour) 991 | { 992 | if (segment.kind == "line") 993 | simplifiedSegments.push(segment) 994 | 995 | else if (segment.kind == "qbezier") 996 | { 997 | for (let i = 0; i < steps; i++) 998 | { 999 | const t1 = (i + 0) / steps 1000 | const t2 = (i + 1) / steps 1001 | 1002 | const x1 = Font.qbezier(t1, segment.x1, segment.x2, segment.x3) 1003 | const y1 = Font.qbezier(t1, segment.y1, segment.y2, segment.y3) 1004 | const x2 = Font.qbezier(t2, segment.x1, segment.x2, segment.x3) 1005 | const y2 = Font.qbezier(t2, segment.y1, segment.y2, segment.y3) 1006 | 1007 | simplifiedSegments.push({ 1008 | kind: "line", 1009 | x1, y1, x2, y2 1010 | }) 1011 | } 1012 | } 1013 | } 1014 | 1015 | simplifiedContours.push(simplifiedSegments) 1016 | } 1017 | 1018 | geometry.contours = simplifiedContours 1019 | return geometry 1020 | } 1021 | 1022 | 1023 | static qbezier(t, p0, p1, p2) 1024 | { 1025 | return (1 - t) * ((1 - t) * p0 + t * p1) + t * ((1 - t) * p1 + t * p2) 1026 | } 1027 | } -------------------------------------------------------------------------------- /src/glyphRangeParser.js: -------------------------------------------------------------------------------- 1 | export function parseGlyphRange(str) 2 | { 3 | let result = { unicodeCodepoints: [], glyphIds: [] } 4 | 5 | try 6 | { 7 | let parser = new Parser(str) 8 | 9 | if (str == "*" || str == "u+*") 10 | return result 11 | 12 | while (!parser.isOver()) 13 | { 14 | if (parser.checkMatch("u+")) 15 | { 16 | const rangeStart = parser.getUnicodeCodepoint() 17 | let rangeEnd = rangeStart 18 | if (parser.tryMatch("..")) 19 | rangeEnd = parser.getUnicodeCodepoint() 20 | 21 | for (let i = rangeStart; i <= rangeEnd; i++) 22 | result.unicodeCodepoints.push(i) 23 | } 24 | else if (parser.checkMatch("#")) 25 | { 26 | const rangeStart = parser.getGlyphId() 27 | let rangeEnd = rangeStart 28 | if (parser.tryMatch("..")) 29 | rangeEnd = parser.getGlyphId() 30 | 31 | for (let i = rangeStart; i <= rangeEnd; i++) 32 | result.glyphIds.push(i) 33 | } 34 | else 35 | throw "" 36 | 37 | if (!parser.tryMatch(",")) 38 | break 39 | } 40 | 41 | if (!parser.isOver()) 42 | throw "" 43 | } 44 | catch 45 | { 46 | throw "error: malformed glyph range" 47 | } 48 | 49 | return result 50 | } 51 | 52 | 53 | class Parser 54 | { 55 | constructor(str) 56 | { 57 | this.str = str 58 | this.index = 0 59 | this.skipWhite() 60 | } 61 | 62 | 63 | isOver() 64 | { 65 | return this.index >= this.str.length 66 | } 67 | 68 | 69 | advance() 70 | { 71 | this.index += 1 72 | this.skipWhite() 73 | } 74 | 75 | 76 | skipWhite() 77 | { 78 | while (isWhitespace(this.cur())) 79 | this.advance() 80 | } 81 | 82 | 83 | cur() 84 | { 85 | return this.next(0) 86 | } 87 | 88 | 89 | next(n) 90 | { 91 | if (this.index + n >= this.str.length) 92 | return "\0" 93 | 94 | return this.str[this.index + n] 95 | } 96 | 97 | 98 | checkMatch(m) 99 | { 100 | for (let i = 0; i < m.length; i++) 101 | { 102 | if (this.next(i) != m[i]) 103 | return false 104 | } 105 | 106 | return true 107 | } 108 | 109 | 110 | tryMatch(m) 111 | { 112 | if (!this.checkMatch(m)) 113 | return false 114 | 115 | for (let i = 0; i < m.length; i++) 116 | this.advance() 117 | 118 | return true 119 | } 120 | 121 | 122 | match(m) 123 | { 124 | if (!this.checkMatch(m)) 125 | throw "expected `" + m + "`" 126 | 127 | for (let i = 0; i < m.length; i++) 128 | this.advance() 129 | } 130 | 131 | 132 | getInt() 133 | { 134 | let value = 0 135 | while (isDigit(this.cur())) 136 | { 137 | value = (value * 10) + getHexDigitValue(this.cur()) 138 | this.advance() 139 | } 140 | 141 | return value 142 | } 143 | 144 | 145 | getHexInt() 146 | { 147 | let value = 0 148 | while (isHexDigit(this.cur())) 149 | { 150 | value = (value * 10) + getHexDigitValue(this.cur()) 151 | this.advance() 152 | } 153 | 154 | return value 155 | } 156 | 157 | 158 | getUnicodeCodepoint() 159 | { 160 | this.match("u+") 161 | 162 | let value = 0 163 | while (isHexDigit(this.cur())) 164 | { 165 | value = (value * 16) + getHexDigitValue(this.cur()) 166 | this.advance() 167 | } 168 | 169 | return value 170 | } 171 | 172 | 173 | getGlyphId() 174 | { 175 | this.match("#") 176 | 177 | let value = 0 178 | while (isDigit(this.cur())) 179 | { 180 | value = (value * 10) + getHexDigitValue(this.cur()) 181 | this.advance() 182 | } 183 | 184 | return value 185 | } 186 | } 187 | 188 | 189 | function isWhitespace(c) 190 | { 191 | const code = c.charCodeAt(0) 192 | return code == " ".charCodeAt(0) 193 | } 194 | 195 | 196 | function isDigit(c) 197 | { 198 | const code = c.charCodeAt(0) 199 | return code >= "0".charCodeAt(0) && code <= "9".charCodeAt(0) 200 | } 201 | 202 | 203 | function isHexDigit(c) 204 | { 205 | const code = c.charCodeAt(0) 206 | return (code >= "0".charCodeAt(0) && code <= "9".charCodeAt(0)) || 207 | (code >= "A".charCodeAt(0) && code <= "F".charCodeAt(0)) || 208 | (code >= "a".charCodeAt(0) && code <= "f".charCodeAt(0)) 209 | } 210 | 211 | 212 | function getHexDigitValue(c) 213 | { 214 | const code = c.charCodeAt(0) 215 | 216 | if (code >= "0".charCodeAt(0) && code <= "9".charCodeAt(0)) 217 | return code - "0".charCodeAt(0) 218 | 219 | else if (code >= "A".charCodeAt(0) && code <= "F".charCodeAt(0)) 220 | return code + 10 - "A".charCodeAt(0) 221 | 222 | else if (code >= "a".charCodeAt(0) && code <= "f".charCodeAt(0)) 223 | return code + 10 - "a".charCodeAt(0) 224 | 225 | else 226 | return 0 227 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { BufferReader } from "@hlorenzi/buffer" 2 | import { Font, FontCollection, GlyphRenderer } from "../index.js" 3 | 4 | 5 | let gFontCollection = null 6 | let gFontIndex = 0 7 | let gRenderGlyphTimeout = null 8 | 9 | 10 | const inputFile = document.getElementById("inputFile") 11 | inputFile.onchange = () => 12 | { 13 | let reader = new FileReader() 14 | reader.onload = () => loadFont(new Uint8Array(reader.result)) 15 | 16 | reader.readAsArrayBuffer(inputFile.files[0]) 17 | } 18 | 19 | document.getElementById("checkboxDrawMetrics").onchange = () => refresh() 20 | document.getElementById("checkboxSortUnicode").onchange = () => refresh() 21 | document.getElementById("checkboxCustomRender").onchange = () => refresh() 22 | 23 | 24 | function loadFont(buffer) 25 | { 26 | try { gFontCollection = FontCollection.fromBytes(buffer) } 27 | catch (e) 28 | { 29 | window.alert("Error loading font!") 30 | throw e 31 | } 32 | 33 | for (const warning of gFontCollection.warnings) 34 | console.warn(warning) 35 | 36 | let selectFontIndex = document.getElementById("selectFontIndex") 37 | while (selectFontIndex.firstChild) 38 | selectFontIndex.removeChild(selectFontIndex.firstChild) 39 | 40 | for (let i = 0; i < gFontCollection.fonts.length; i++) 41 | { 42 | const familyName = gFontCollection.fonts[i].fontFamilyName 43 | const subfamilyName = gFontCollection.fonts[i].fontSubfamilyName 44 | const fullName = "[" + i + "] " + 45 | (familyName == null || subfamilyName == null ? "" : familyName + " " + subfamilyName) 46 | 47 | let option = document.createElement("option") 48 | option.innerHTML = fullName 49 | option.value = i 50 | selectFontIndex.appendChild(option) 51 | } 52 | 53 | selectFontIndex.selected = gFontIndex = 0 54 | selectFontIndex.disabled = (gFontCollection.fonts.length <= 1) 55 | selectFontIndex.onchange = () => 56 | { 57 | gFontIndex = parseInt(selectFontIndex.value) 58 | refresh() 59 | } 60 | 61 | console.log(gFontCollection) 62 | refresh() 63 | } 64 | 65 | 66 | function refresh() 67 | { 68 | buildGlyphList() 69 | } 70 | 71 | 72 | function buildGlyphList() 73 | { 74 | let divGlyphList = document.getElementById("divGlyphList") 75 | while (divGlyphList.firstChild) 76 | divGlyphList.removeChild(divGlyphList.firstChild) 77 | 78 | if (gFontCollection == null) 79 | return 80 | 81 | let font = gFontCollection.getFont(gFontIndex) 82 | 83 | const unicodeMap = font.getUnicodeMap() 84 | let glyphToUnicodeMap = new Map() 85 | for (const [code, glyphId] of unicodeMap) 86 | { 87 | if (!glyphToUnicodeMap.has(glyphId)) 88 | glyphToUnicodeMap.set(glyphId, code) 89 | } 90 | 91 | const sortByUnicode = document.getElementById("checkboxSortUnicode").checked 92 | 93 | const glyphCount = font.getGlyphCount() 94 | 95 | let glyphSlotsToAdd = [] 96 | 97 | if (!sortByUnicode) 98 | { 99 | for (let glyphId = 0; glyphId < glyphCount; glyphId++) 100 | glyphSlotsToAdd.push({ glyphId, unicodeIndex: glyphToUnicodeMap.get(glyphId) }) 101 | } 102 | else 103 | { 104 | let availableUnicode = [] 105 | for (const [code, glyphId] of unicodeMap) 106 | availableUnicode.push(code) 107 | 108 | let availableGlyphsSet = new Set() 109 | for (let glyphId = 0; glyphId < glyphCount; glyphId++) 110 | availableGlyphsSet.add(glyphId) 111 | 112 | let prevUnicodeAdded = -1 113 | availableUnicode.sort((a, b) => a - b) 114 | for (const code of availableUnicode) 115 | { 116 | if (code != prevUnicodeAdded + 1 && prevUnicodeAdded >= 0) 117 | glyphSlotsToAdd.push({ glyphId: null, unicodeIndex: null }) 118 | 119 | prevUnicodeAdded = code 120 | 121 | const glyphId = unicodeMap.get(code) 122 | glyphSlotsToAdd.push({ glyphId, unicodeIndex: code }) 123 | availableGlyphsSet.delete(glyphId) 124 | } 125 | 126 | let availableGlyphs = [] 127 | for (const glyphId of availableGlyphsSet) 128 | availableGlyphs.push(glyphId) 129 | 130 | availableGlyphs.sort((a, b) => a - b) 131 | 132 | if (availableGlyphs.length > 0) 133 | glyphSlotsToAdd.push({ glyphId: null, unicodeIndex: null }) 134 | 135 | for (const glyphId of availableGlyphs) 136 | glyphSlotsToAdd.push({ glyphId, unicodeIndex: null }) 137 | } 138 | 139 | if (gRenderGlyphTimeout != null) 140 | window.clearTimeout(gRenderGlyphTimeout) 141 | 142 | const useCustomRasterizer = document.getElementById("checkboxCustomRender").checked 143 | addGlyphSlotIterator(glyphSlotsToAdd, useCustomRasterizer ? 5 : 1000) 144 | } 145 | 146 | 147 | function addGlyphSlotIterator(glyphSlotsToAdd, countPerIteration, i = 0) 148 | { 149 | let count = countPerIteration 150 | while (count > 0) 151 | { 152 | count-- 153 | if (i >= glyphSlotsToAdd.length) 154 | return 155 | 156 | addGlyphSlot(glyphSlotsToAdd[i].glyphId, glyphSlotsToAdd[i].unicodeIndex) 157 | i++ 158 | } 159 | 160 | gRenderGlyphTimeout = window.setTimeout(() => addGlyphSlotIterator(glyphSlotsToAdd, countPerIteration, i), 0) 161 | } 162 | 163 | 164 | function addGlyphSlot(glyphId, unicodeIndex) 165 | { 166 | let font = gFontCollection.getFont(gFontIndex) 167 | 168 | let glyphListItem = document.createElement("div") 169 | glyphListItem.className = "glyphListItem" 170 | 171 | let glyphLabel = "..." 172 | if (glyphId != null) 173 | glyphLabel = "#" + glyphId.toString() + " (U+" + (unicodeIndex == null ? "????" : unicodeIndex.toString(16).padStart(4, "0")) + ")" 174 | 175 | let glyphListItemLabel = document.createElement("div") 176 | glyphListItemLabel.className = "glyphListItemLabel" 177 | glyphListItemLabel.innerHTML = glyphLabel 178 | 179 | let glyphListItemCanvas = document.createElement("canvas") 180 | glyphListItemCanvas.className = "glyphListItemCanvas" 181 | glyphListItemCanvas.width = "100" 182 | glyphListItemCanvas.height = "100" 183 | 184 | glyphListItem.appendChild(glyphListItemLabel) 185 | glyphListItem.appendChild(glyphListItemCanvas) 186 | divGlyphList.appendChild(glyphListItem) 187 | 188 | glyphListItem.ondblclick = () => 189 | { 190 | if (glyphId == null) 191 | return 192 | 193 | console.log("Data for glyph " + glyphLabel + ":") 194 | console.log(font.getGlyphData(glyphId)) 195 | console.log(font.getGlyphGeometry(glyphId)) 196 | 197 | if (unicodeIndex != null) 198 | window.open("https://r12a.github.io/uniview/?charlist=" + String.fromCodePoint(unicodeIndex), "_blank") 199 | } 200 | 201 | let ctx = glyphListItemCanvas.getContext("2d") 202 | 203 | const drawMetrics = document.getElementById("checkboxDrawMetrics").checked 204 | const useCustomRasterizer = document.getElementById("checkboxCustomRender").checked 205 | 206 | const lineMetrics = font.getHorizontalLineMetrics() 207 | 208 | const geometry = (glyphId == null ? null : font.getGlyphGeometry(glyphId)) 209 | 210 | if (useCustomRasterizer) 211 | renderGlyphGeometryCustom(ctx, geometry, lineMetrics, drawMetrics) 212 | else 213 | renderGlyphGeometry(ctx, geometry, lineMetrics, drawMetrics) 214 | } 215 | 216 | 217 | function renderGlyphGeometryCustom(ctx, geometry, lineMetrics, drawMetrics) 218 | { 219 | ctx.fillStyle = (geometry == null ? "#fee" : geometry.isComposite ? "#f8eeff" : "#fff") 220 | ctx.fillRect(-100, -100, 200, 200) 221 | 222 | if (geometry == null) 223 | return 224 | 225 | const img = GlyphRenderer.render(Font.simplifyBezierContours(geometry), 90) 226 | 227 | ctx.fillStyle = "#f00" 228 | for (let y = 0; y < 100; y++) 229 | { 230 | for (let x = 0; x < 100; x++) 231 | { 232 | if ((x + y) % 2 == 0) 233 | ctx.fillRect(x, y, 1, 1) 234 | } 235 | } 236 | 237 | ctx.fillStyle = "#000" 238 | for (let y = 0; y < img.height; y++) 239 | { 240 | for (let x = 0; x < img.width; x++) 241 | { 242 | const value = img.buffer[y * img.width + x] 243 | ctx.fillStyle = value > 0 ? "#fff" : "#000" 244 | ctx.fillRect(x, y, 1, 1) 245 | } 246 | } 247 | } 248 | 249 | 250 | function renderGlyphGeometry(ctx, geometry, lineMetrics, drawMetrics) 251 | { 252 | ctx.fillStyle = (geometry == null ? "#fee" : geometry.isComposite ? "#f8eeff" : "#fff") 253 | ctx.fillRect(-100, -100, 200, 200) 254 | 255 | if (geometry == null) 256 | return 257 | 258 | const scale = 1 / (lineMetrics.lineBottom - lineMetrics.lineTop + lineMetrics.lineGap * 2) * 90 259 | 260 | ctx.translate(50, 50) 261 | ctx.scale(scale, scale) 262 | 263 | if (drawMetrics) 264 | ctx.translate(-geometry.advance / 2, -(lineMetrics.lineTop + lineMetrics.lineBottom) / 2) 265 | else 266 | ctx.translate(-(geometry.xMax + geometry.xMin) / 2, -(geometry.yMax + geometry.yMin) / 2) 267 | 268 | if (drawMetrics) 269 | { 270 | ctx.fillStyle = "#fb8" 271 | ctx.fillRect(-1000, -1000, 2000, lineMetrics.lineTop + 1000) 272 | ctx.fillRect(-1000, lineMetrics.lineBottom, 2000, 1000) 273 | 274 | ctx.fillStyle = "#f96" 275 | ctx.fillRect(-1000, -1000, 2000, lineMetrics.lineTop - lineMetrics.lineGap + 1000) 276 | ctx.fillRect(-1000, lineMetrics.lineBottom + lineMetrics.lineGap, 2000, 1000) 277 | 278 | ctx.lineWidth = 1 / scale 279 | 280 | ctx.strokeStyle = "#080" 281 | ctx.beginPath() 282 | ctx.moveTo(-1000, 0) 283 | ctx.lineTo( 1000, 0) 284 | ctx.moveTo(0, -1000) 285 | ctx.lineTo(0, 1000) 286 | ctx.stroke() 287 | 288 | ctx.strokeStyle = "#00f" 289 | ctx.beginPath() 290 | ctx.moveTo(geometry.advance, -1000) 291 | ctx.lineTo(geometry.advance, 1000) 292 | ctx.stroke() 293 | } 294 | 295 | ctx.fillStyle = "#000" 296 | ctx.beginPath() 297 | 298 | for (const contour of geometry.contours) 299 | { 300 | ctx.moveTo(contour[0].x1, contour[0].y1) 301 | 302 | for (const segment of contour) 303 | { 304 | if (segment.kind == "line") 305 | ctx.lineTo(segment.x2, segment.y2) 306 | 307 | else if (segment.kind == "qbezier") 308 | ctx.quadraticCurveTo(segment.x2, segment.y2, segment.x3, segment.y3) 309 | } 310 | 311 | ctx.lineTo(contour[0].x1, contour[0].y1) 312 | } 313 | 314 | ctx.fill() 315 | } -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | export class GlyphImage 2 | { 3 | constructor(width, height, emScale, xOrigin, yOrigin) 4 | { 5 | this.width = width 6 | this.height = height 7 | this.buffer = new Float64Array(width * height) 8 | this.emScale = emScale 9 | this.xOrigin = xOrigin 10 | this.yOrigin = yOrigin 11 | } 12 | 13 | 14 | setPixel(x, y, c) 15 | { 16 | if (x < 0 || y < 0 || x >= this.width || y >= this.height) 17 | return 18 | 19 | this.buffer[y * this.width + x] = c 20 | } 21 | 22 | 23 | getPixel(x, y) 24 | { 25 | if (x < 0 || y < 0 || x >= this.width || y >= this.height) 26 | return 0 27 | 28 | return this.buffer[y * this.width + x] 29 | } 30 | 31 | 32 | mapPixels(fn) 33 | { 34 | for (let y = 0; y < this.height; y++) 35 | for (let x = 0; x < this.width; x++) 36 | this.buffer[y * this.width + x] = fn(this.buffer[y * this.width + x]) 37 | } 38 | 39 | 40 | binarize(cutoff = 0.5) 41 | { 42 | this.mapPixels(c => c > cutoff ? 1 : 0) 43 | } 44 | 45 | 46 | outline(min, max) 47 | { 48 | this.mapPixels(c => c >= min && c <= max ? 1 : 0) 49 | } 50 | 51 | 52 | normalizeColorRange() 53 | { 54 | this.mapPixels(c => Math.max(0, Math.min(255, Math.floor(c * 255)))) 55 | } 56 | 57 | 58 | normalizeSignedDistance(rangeMin, rangeMax) 59 | { 60 | this.mapPixels(c => 1 - (c - rangeMin) / (rangeMax - rangeMin)) 61 | } 62 | 63 | 64 | getDownsampled(gamma = 2.2) 65 | { 66 | const newImage = new GlyphImage(Math.floor(this.width / 16), Math.floor(this.height / 16), this.emScale / 16, this.xOrigin / 16, this.yOrigin / 16) 67 | 68 | for (let y = 0; y < newImage.height; y++) 69 | for (let x = 0; x < newImage.width; x++) 70 | { 71 | let accum = 0 72 | for (let yy = 0; yy < 16; yy++) 73 | for (let xx = 0; xx < 16; xx++) 74 | accum += this.getPixel(x * 16 + xx, y * 16 + yy) 75 | 76 | newImage.setPixel(x, y, Math.pow(accum / 256, 1 / gamma)) 77 | } 78 | 79 | return newImage 80 | } 81 | 82 | 83 | getWithBorder(borderSize) 84 | { 85 | const newImage = new GlyphImage(this.width + borderSize * 2, this.height + borderSize * 2, this.emScale, this.xOrigin + borderSize, this.yOrigin + borderSize) 86 | 87 | for (let y = 0; y < newImage.height; y++) 88 | for (let x = 0; x < newImage.width; x++) 89 | newImage.setPixel(x, y, this.getPixel(x - borderSize, y - borderSize)) 90 | 91 | return newImage 92 | } 93 | 94 | 95 | crop(wNew, hNew, xFrom, yFrom) 96 | { 97 | const newImage = new GlyphImage(wNew, hNew, this.emScale, this.xOrigin + xFrom, this.yOrigin + yFrom) 98 | 99 | for (let y = 0; y < newImage.height; y++) 100 | for (let x = 0; x < newImage.width; x++) 101 | newImage.setPixel(x, y, this.getPixel(x + xFrom, y + yFrom)) 102 | 103 | return newImage 104 | } 105 | 106 | 107 | cropped() 108 | { 109 | let found = false 110 | 111 | let xMin = 0 112 | for (let x = 0; x < this.width && !found; x++) 113 | { 114 | xMin = x 115 | for (let y = 0; y < this.height && !found; y++) 116 | { 117 | if (this.getPixel(x, y) > 0) 118 | found = true 119 | } 120 | } 121 | 122 | found = false 123 | let xMax = this.width - 1 124 | for (let x = this.width - 1; x >= 0 && !found; x--) 125 | { 126 | xMax = x 127 | for (let y = 0; y < this.height && !found; y++) 128 | { 129 | if (this.getPixel(x, y) > 0) 130 | found = true 131 | } 132 | } 133 | 134 | found = false 135 | let yMin = 0 136 | for (let y = 0; y < this.height && !found; y++) 137 | { 138 | yMin = y 139 | for (let x = 0; x < this.width && !found; x++) 140 | { 141 | if (this.getPixel(x, y) > 0) 142 | found = true 143 | } 144 | } 145 | 146 | found = false 147 | let yMax = this.height - 1 148 | for (let y = this.height - 1; y >= 0 && !found; y--) 149 | { 150 | yMax = y 151 | for (let x = 0; x < this.width && !found; x++) 152 | { 153 | if (this.getPixel(x, y) > 0) 154 | found = true 155 | } 156 | } 157 | 158 | return this.crop(xMax + 1 - xMin, yMax + 1 - yMin, xMin, yMin) 159 | } 160 | 161 | 162 | getSignedDistanceField() 163 | { 164 | const calcDist = (obj) => Math.sqrt(obj.dx * obj.dx + obj.dy * obj.dy) 165 | const calcDistSqr = (obj) => (obj.dx * obj.dx + obj.dy * obj.dy) 166 | 167 | const getUnsignedDF = (invert) => 168 | { 169 | const distanceBuffer = [] 170 | for (let y = 0; y < this.height; y++) 171 | for (let x = 0; x < this.width; x++) 172 | distanceBuffer.push((invert ? this.getPixel(x, y) < 0.5 : this.getPixel(x, y) >= 0.5) ? { dx: 0, dy: 0 } : { dx: Infinity, dy: Infinity }) 173 | 174 | const compare = (dCur, x, y, xOff, yOff) => 175 | { 176 | const xx = x + xOff 177 | const yy = y + yOff 178 | if (xx < 0 || yy < 0 || xx >= this.width || yy >= this.height) 179 | return 180 | 181 | const dOther = distanceBuffer[yy * this.width + xx] 182 | const dNew = { dx: dOther.dx + xOff, dy: dOther.dy + yOff } 183 | if (calcDistSqr(dNew) < calcDistSqr(dCur)) 184 | { 185 | dCur.dx = dNew.dx 186 | dCur.dy = dNew.dy 187 | } 188 | } 189 | 190 | for (let y = 0; y < this.height; y++) 191 | { 192 | for (let x = 0; x < this.width; x++) 193 | { 194 | const d = distanceBuffer[y * this.width + x] 195 | compare(d, x, y, -1, 0) 196 | compare(d, x, y, 0, -1) 197 | compare(d, x, y, -1, -1) 198 | compare(d, x, y, 1, -1) 199 | distanceBuffer[y * this.width + x] = d 200 | } 201 | 202 | for (let x = this.width - 1; x >= 0; x--) 203 | { 204 | const d = distanceBuffer[y * this.width + x] 205 | compare(d, x, y, 1, 0) 206 | distanceBuffer[y * this.width + x] = d 207 | } 208 | } 209 | 210 | for (let y = this.height - 1; y >= 0; y--) 211 | { 212 | for (let x = this.width - 1; x >= 0; x--) 213 | { 214 | const d = distanceBuffer[y * this.width + x] 215 | compare(d, x, y, 1, 0) 216 | compare(d, x, y, 0, 1) 217 | compare(d, x, y, -1, 1) 218 | compare(d, x, y, 1, 1) 219 | distanceBuffer[y * this.width + x] = d 220 | } 221 | 222 | for (let x = 0; x < this.width; x++) 223 | { 224 | const d = distanceBuffer[y * this.width + x] 225 | compare(d, x, y, -1, 0) 226 | distanceBuffer[y * this.width + x] = d 227 | } 228 | } 229 | 230 | return distanceBuffer 231 | } 232 | 233 | const outsideDF = getUnsignedDF(false) 234 | const insideDF = getUnsignedDF(true) 235 | 236 | const newImage = new GlyphImage(this.width, this.height, this.emScale, this.xOrigin, this.yOrigin) 237 | 238 | for (let y = 0; y < this.height; y++) 239 | for (let x = 0; x < this.width; x++) 240 | { 241 | const outsideD = calcDist(outsideDF[y * this.width + x]) 242 | const insideD = calcDist(insideDF [y * this.width + x]) 243 | newImage.setPixel(x, y, outsideD <= 0 ? -insideD : outsideD) 244 | } 245 | 246 | return newImage 247 | } 248 | 249 | 250 | getSignedDistanceFieldSlow() 251 | { 252 | const newImage = new GlyphImage(this.width, this.height, this.emScale, this.xOrigin, this.yOrigin) 253 | 254 | for (let y = 0; y < this.height; y++) 255 | for (let x = 0; x < this.width; x++) 256 | { 257 | let minDist = Infinity 258 | 259 | const c1 = this.getPixel(x, y) 260 | 261 | const testPixel = (xx, yy) => 262 | { 263 | const dx = xx - x 264 | const dy = yy - y 265 | const d = dx * dx + dy * dy 266 | if (d >= minDist * minDist) 267 | return 268 | 269 | const c2 = this.getPixel(xx, yy) 270 | 271 | if (c1 < 0.5 && c2 < 0.5) 272 | return 273 | 274 | if (c1 >= 0.5 && c2 >= 0.5) 275 | return 276 | 277 | minDist = Math.sqrt(d) 278 | } 279 | 280 | for (let t = 0; t < Math.max(this.width, this.height) && t < minDist; t++) 281 | { 282 | for (let yy = -t; yy <= t; yy++) 283 | { 284 | testPixel(x - t, y + yy) 285 | testPixel(x + t, y + yy) 286 | } 287 | 288 | for (let xx = -t; xx <= t; xx++) 289 | { 290 | testPixel(x + xx, y - t) 291 | testPixel(x + xx, y + t) 292 | } 293 | } 294 | 295 | const dSigned = (c1 >= 0.5 ? -1 : 1) * minDist 296 | newImage.setPixel(x, y, dSigned) 297 | } 298 | 299 | return newImage 300 | } 301 | 302 | 303 | printToString() 304 | { 305 | let str = "" 306 | for (let y = 0; y < this.height; y++) 307 | { 308 | for (let x = 0; x < this.width; x++) 309 | { 310 | const c = this.getPixel(x, y) 311 | 312 | if (c > 0.8) str += "█" 313 | else if (c > 0.6) str += "▓" 314 | else if (c > 0.4) str += "▒" 315 | else if (c > 0.2) str += "░" 316 | else str += " " 317 | } 318 | 319 | if (y < this.height - 1) 320 | str += "\n" 321 | } 322 | 323 | return str 324 | } 325 | } 326 | 327 | 328 | export class GlyphRenderer 329 | { 330 | static render(geometry, emToPixelSize) 331 | { 332 | for (const contour of geometry.contours) 333 | for (const edge of contour) 334 | if (edge.kind != "line") 335 | throw "can only render geometries consisting of straight lines; try using the `simplifySteps` argument of `Font#getGlyphGeometry`" 336 | 337 | const snap = (x, s) => Math.floor(x * s) / s 338 | 339 | const glyphEmW = geometry.xMax - geometry.xMin 340 | const glyphEmH = geometry.yMax - geometry.yMin 341 | 342 | const pixelW = snap(Math.ceil(glyphEmW * emToPixelSize) + 48, 16) 343 | const pixelH = snap(Math.ceil(glyphEmH * emToPixelSize) + 48, 16) 344 | 345 | const pixelOffsetX = Math.ceil(-geometry.xMin * emToPixelSize) + 16 346 | const pixelOffsetY = Math.ceil(-geometry.yMin * emToPixelSize) + 16 347 | 348 | const render = new GlyphImage(pixelW, pixelH, emToPixelSize, pixelOffsetX, pixelOffsetY) 349 | 350 | let intersectingEdges = [] 351 | for (const contour of geometry.contours) 352 | { 353 | for (const edge of contour) 354 | { 355 | if (edge.y1 == edge.y2) 356 | continue 357 | 358 | intersectingEdges.push({ 359 | x1: edge.x1, 360 | y1: edge.y1, 361 | x2: edge.x2, 362 | y2: edge.y2, 363 | xAtCurrentScanline: 0, 364 | winding: 0 365 | }) 366 | } 367 | } 368 | 369 | for (let y = 0; y < pixelH; y++) 370 | { 371 | const yEmSpace = (y - pixelOffsetY) / emToPixelSize 372 | 373 | for (let edge of intersectingEdges) 374 | { 375 | if (Math.min(edge.y1, edge.y2) >= yEmSpace || Math.max(edge.y1, edge.y2) < yEmSpace) 376 | { 377 | edge.xAtCurrentScanline = 0 378 | edge.winding = 0 379 | continue 380 | } 381 | 382 | edge.xAtCurrentScanline = edge.x1 + ((yEmSpace - edge.y1) / (edge.y2 - edge.y1)) * (edge.x2 - edge.x1) 383 | edge.winding = (edge.y2 > edge.y1) ? 1 : -1 384 | } 385 | 386 | intersectingEdges.sort((a, b) => a.xAtCurrentScanline - b.xAtCurrentScanline) 387 | 388 | let currentWinding = 0 389 | let currentIntersection = 0 390 | for (let x = 0; x < pixelW; x++) 391 | { 392 | const xEmSpace = (x - pixelOffsetX) / emToPixelSize 393 | 394 | while (currentIntersection < intersectingEdges.length && 395 | intersectingEdges[currentIntersection].xAtCurrentScanline <= xEmSpace) 396 | { 397 | currentWinding += intersectingEdges[currentIntersection].winding 398 | currentIntersection += 1 399 | } 400 | 401 | render.setPixel(x, y, (currentWinding == 0 ? 0 : 1)) 402 | } 403 | } 404 | 405 | return render 406 | } 407 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import { FontCollection, GlyphRenderer } from "./index.js" 2 | import fs from "fs" 3 | import assert from "assert" 4 | 5 | 6 | const fontBuffer = fs.readFileSync("C:/Windows/Fonts/arial.ttf") 7 | 8 | const fontCollection = FontCollection.fromBytes(fontBuffer) 9 | assert(fontCollection.fonts.length > 0) 10 | 11 | const font = fontCollection.fonts[0] 12 | assert.equal(font.fontFamilyName, "Arial") 13 | assert.equal(font.fontSubfamilyName, "Regular") 14 | 15 | const glyphIndex = font.getGlyphIndexForUnicode("a".charCodeAt(0)) 16 | const glyphGeometry = font.getGlyphGeometry(glyphIndex, 100) 17 | 18 | assert.equal(glyphGeometry.contours.length, 2) 19 | 20 | const glyphImage = GlyphRenderer.render(glyphGeometry, 12 * 16).getDownsampled().cropped() 21 | 22 | console.log("") 23 | console.log("This should look like an Arial low-resolution lowercase `a`:") 24 | console.log(glyphImage.printToString()) 25 | console.log("") 26 | 27 | glyphImage.normalizeColorRange() 28 | 29 | assert.deepEqual([...glyphImage.buffer], 30 | [ 31 | 78, 209, 239, 242, 216, 74, 32 | 198, 199, 28, 52, 224, 172, 33 | 33, 65, 114, 152, 226, 186, 34 | 156, 243, 218, 192, 219, 186, 35 | 239, 147, 0, 0, 216, 186, 36 | 219, 210, 147, 194, 243, 192, 37 | 74, 177, 191, 141, 109, 151 38 | ]) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | 4 | module.exports = 5 | { 6 | mode: "production", 7 | entry: 8 | { 9 | main: path.resolve(__dirname, "src/main.js"), 10 | }, 11 | 12 | output: 13 | { 14 | filename: "[name].js", 15 | path: path.resolve(__dirname, "webpack") 16 | } 17 | } -------------------------------------------------------------------------------- /webpack/main.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(i){if(t[i])return t[i].exports;var r=t[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(i,r,function(t){return e[t]}.bind(null,r));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);class i{constructor(e){this.bytes=e,this.head=0}get length(){return this.bytes.length}_at(e){const t=this.bytes[e];if(void 0===t)throw new RangeError;return t}seek(e){this.head=e}peekUint8(){return this._at(this.head)}readUint8(){const e=this._at(this.head);return this.head+=1,e}readManyUint8(e){let t=[];for(let n=0;nt.tableTag==e);if(null==t)throw"missing table `"+e+"`";return t}get usesTrueTypeOutlines(){return"OTTO"!=this.sfntVersion}readOffsetTable(e){this.sfntVersion=e.readAsciiLength(4),this.numTables=e.readUint16BE(),this.searchRange=e.readUint16BE(),this.entrySelector=e.readUint16BE(),this.rangeShift=e.readUint16BE()}readTableRecord(e){this.tables=[];for(let t=0;t{let n=null;for(let i of t.nameRecords)if(null!=i.string&&i.nameID==e){if(0==i.languageID)return i.string;n=i.string}return n};this.fontCopyright=i(0),this.fontFamilyName=i(1),this.fontSubfamilyName=i(2),this.fontUniqueIdentifier=i(3),this.fontFullName=i(4),this.fontVersionString=i(5),this.fontPostScriptName=i(6),this.fontTrademark=i(7),this.fontManufacturer=i(8)}readHorizontalHeaderTable(e){let t=this.getTable("hhea");e.seek(t.offset),t.majorVersion=e.readUint16BE(),t.minorVersion=e.readUint16BE(),t.ascender=e.readInt16BE(),t.descender=e.readInt16BE(),t.lineGap=e.readInt16BE(),t.advanceWidthMax=e.readUint16BE(),t.minLeftSideBearing=e.readInt16BE(),t.minRightSideBearing=e.readInt16BE(),t.xMaxExtent=e.readInt16BE(),t.caretSlopeRise=e.readInt16BE(),t.caretSlopeRun=e.readInt16BE(),t.caretOffset=e.readInt16BE(),t.reserved0=e.readInt16BE(),t.reserved1=e.readInt16BE(),t.reserved2=e.readInt16BE(),t.reserved3=e.readInt16BE(),t.metricDataFormat=e.readInt16BE(),t.numberOfHMetrics=e.readUint16BE()}readHorizontalMetricsTable(e){const t=this.getTable("hhea"),n=this.getTable("maxp");let i=this.getTable("hmtx");e.seek(i.offset),i.hMetrics=[];for(let n=0;n2*e):1==t.indexToLocFormat&&(i.offsets=e.readManyUint32BE(r));for(let e=1;e=0?this.readGlyphSimple(e,n):this.readGlyphComposite(e,n,t),n}readGlyphSimple(e,t){t.endPtsOfContours=e.readManyUint16BE(t.numberOfContours);for(let e=1;et.endCode[r]||i>14;return(2&n?2-n:n)+(16383&t)/16384}fetchGlyphOnDemand(e,t){const n=this.getTable("maxp"),i=this.getTable("loca"),r=this.getTable("glyf");return t<0||t>n.numGlyphs?null:null==i.offsets[t]?null:(e.seek(r.offset+i.offsets[t]),this.readGlyph(e,t))}getGlyphData(e){const t=this.getTable("glyf");return t.glyphs?t.glyphs[e]:this.fetchGlyphOnDemand(this.data,e)}getGlyphGeometry(e,t=0){const n=this.getTable("head"),i=this.getTable("hmtx"),r=this.getGlyphData(e),s=1/n.unitsPerEm;let o={contours:[],xMin:null,yMin:null,xMax:null,yMax:null},l=null;l=e>=i.hMetrics.length?i.hMetrics[i.hMetrics.length-1]:i.hMetrics[e],o.advance=l?l.advanceWidth/n.unitsPerEm:0;const d=(e,t)=>{o.xMin=null==o.xMin?e:Math.min(o.xMin,e),o.xMax=null==o.xMax?e:Math.max(o.xMax,e),o.yMin=null==o.yMin?t:Math.min(o.yMin,t),o.yMax=null==o.yMax?t:Math.max(o.yMax,t)};if(null!=r){if(o.isComposite=r.numberOfContours<=0,o.isComposite){const e=2;for(const t of r.components){const n=this.getGlyphGeometry(t.glyphIndex);if(null==n)continue;if(0==(t.flags&e))return null;const i=s*t.argument1,r=-s*t.argument2;for(const e of n.contours){for(const n of e){const e=n.x1*t.xScale+n.y1*t.scale01+i,a=n.y1*t.yScale+n.x1*t.scale10+r,s=n.x2*t.xScale+n.y2*t.scale01+i,o=n.y2*t.yScale+n.x2*t.scale10+r;if(n.x1=e,n.y1=a,n.x2=s,n.y2=o,"qbezier"==n.kind){const e=n.x3*t.xScale+n.y3*t.scale01+i,a=n.y3*t.yScale+n.x3*t.scale10+r;n.x3=e,n.y3=a}}o.contours.push(e)}}}else for(let e=0;e0&&(o=a.simplifyBezierContours(o,t)),o}static simplifyBezierContours(e,t=100){if(null==e)return e;let n=[];for(const i of e.contours){let e=[];for(const n of i)if("line"==n.kind)e.push(n);else if("qbezier"==n.kind)for(let i=0;i=this.width||t>=this.height||(this.buffer[t*this.width+e]=n)}getPixel(e,t){return e<0||t<0||e>=this.width||t>=this.height?0:this.buffer[t*this.width+e]}mapPixels(e){for(let t=0;tt>e?1:0)}outline(e,t){this.mapPixels(n=>n>=e&&n<=t?1:0)}normalizeColorRange(){this.mapPixels(e=>Math.max(0,Math.min(255,Math.floor(255*e))))}normalizeSignedDistance(e,t){this.mapPixels(n=>1-(n-e)/(t-e))}getDownsampled(e=2.2){const t=new s(Math.floor(this.width/16),Math.floor(this.height/16),this.emScale/16,this.xOrigin/16,this.yOrigin/16);for(let n=0;n0&&(e=!0)}e=!1;let n=this.width-1;for(let t=this.width-1;t>=0&&!e;t--){n=t;for(let n=0;n0&&(e=!0)}e=!1;let i=0;for(let t=0;t0&&(e=!0)}e=!1;let r=this.height-1;for(let t=this.height-1;t>=0&&!e;t--){r=t;for(let n=0;n0&&(e=!0)}return this.crop(n+1-t,r+1-i,t,i)}getSignedDistanceField(){const e=e=>Math.sqrt(e.dx*e.dx+e.dy*e.dy),t=e=>e.dx*e.dx+e.dy*e.dy,n=e=>{const n=[];for(let t=0;t=.5)?{dx:0,dy:0}:{dx:1/0,dy:1/0});const i=(e,i,r,a,s)=>{const o=i+a,l=r+s;if(o<0||l<0||o>=this.width||l>=this.height)return;const d=n[l*this.width+o],h={dx:d.dx+a,dy:d.dy+s};t(h)=0;t--){const r=n[e*this.width+t];i(r,t,e,1,0),n[e*this.width+t]=r}}for(let e=this.height-1;e>=0;e--){for(let t=this.width-1;t>=0;t--){const r=n[e*this.width+t];i(r,t,e,1,0),i(r,t,e,0,1),i(r,t,e,-1,1),i(r,t,e,1,1),n[e*this.width+t]=r}for(let t=0;t{const s=e-n,o=a-t,l=s*s+o*o;if(l>=i*i)return;const d=this.getPixel(e,a);r<.5&&d<.5||r>=.5&&d>=.5||(i=Math.sqrt(l))};for(let e=0;e=.5?-1:1)*i;e.setPixel(n,t,s)}return e}printToString(){let e="";for(let t=0;t.8?"█":i>.6?"▓":i>.4?"▒":i>.2?"░":" "}tMath.floor(e*t)/t,i=e.xMax-e.xMin,r=e.yMax-e.yMin,a=n(Math.ceil(i*t)+48,16),o=n(Math.ceil(r*t)+48,16),l=Math.ceil(-e.xMin*t)+16,d=Math.ceil(-e.yMin*t)+16,h=new s(a,o,t,l,d);let f=[];for(const t of e.contours)for(const e of t)e.y1!=e.y2&&f.push({x1:e.x1,y1:e.y1,x2:e.x2,y2:e.y2,xAtCurrentScanline:0,winding:0});for(let e=0;e=n||Math.max(e.y1,e.y2)e.y1?1:-1);f.sort((e,t)=>e.xAtCurrentScanline-t.xAtCurrentScanline);let i=0,r=0;for(let n=0;ne-t);for(const r of e){r!=i+1&&i>=0&&s.push({glyphId:null,unicodeIndex:null}),i=r;const e=n.get(r);s.push({glyphId:e,unicodeIndex:r}),t.delete(e)}let r=[];for(const e of t)r.push(e);r.sort((e,t)=>e-t),r.length>0&&s.push({glyphId:null,unicodeIndex:null});for(const e of r)s.push({glyphId:e,unicodeIndex:null})}else for(let e=0;e0;){if(r--,i>=t.length)return;u(t[i].glyphId,t[i].unicodeIndex),i++}h=window.setTimeout(()=>e(t,n,i),0)}(s,o?5:1e3)}()}function u(e,t){let n=l.getFont(d),i=document.createElement("div");i.className="glyphListItem";let r="...";null!=e&&(r="#"+e.toString()+" (U+"+(null==t?"????":t.toString(16).padStart(4,"0"))+")");let s=document.createElement("div");s.className="glyphListItemLabel",s.innerHTML=r;let h=document.createElement("canvas");h.className="glyphListItemCanvas",h.width="100",h.height="100",i.appendChild(s),i.appendChild(h),divGlyphList.appendChild(i),i.ondblclick=()=>{null!=e&&(console.log("Data for glyph "+r+":"),console.log(n.getGlyphData(e)),console.log(n.getGlyphGeometry(e)),null!=t&&window.open("https://r12a.github.io/uniview/?charlist="+String.fromCodePoint(t),"_blank"))};let f=h.getContext("2d");const c=document.getElementById("checkboxDrawMetrics").checked,u=document.getElementById("checkboxCustomRender").checked,g=n.getHorizontalLineMetrics(),y=null==e?null:n.getGlyphGeometry(e);u?function(e,t,n,i){if(e.fillStyle=null==t?"#fee":t.isComposite?"#f8eeff":"#fff",e.fillRect(-100,-100,200,200),null==t)return;const r=o.render(a.simplifyBezierContours(t),90);e.fillStyle="#f00";for(let t=0;t<100;t++)for(let n=0;n<100;n++)(n+t)%2==0&&e.fillRect(n,t,1,1);e.fillStyle="#000";for(let t=0;t0?"#fff":"#000",e.fillRect(n,t,1,1)}}(f,y):function(e,t,n,i){if(e.fillStyle=null==t?"#fee":t.isComposite?"#f8eeff":"#fff",e.fillRect(-100,-100,200,200),null==t)return;const r=1/(n.lineBottom-n.lineTop+2*n.lineGap)*90;e.translate(50,50),e.scale(r,r),i?e.translate(-t.advance/2,-(n.lineTop+n.lineBottom)/2):e.translate(-(t.xMax+t.xMin)/2,-(t.yMax+t.yMin)/2);i&&(e.fillStyle="#fb8",e.fillRect(-1e3,-1e3,2e3,n.lineTop+1e3),e.fillRect(-1e3,n.lineBottom,2e3,1e3),e.fillStyle="#f96",e.fillRect(-1e3,-1e3,2e3,n.lineTop-n.lineGap+1e3),e.fillRect(-1e3,n.lineBottom+n.lineGap,2e3,1e3),e.lineWidth=1/r,e.strokeStyle="#080",e.beginPath(),e.moveTo(-1e3,0),e.lineTo(1e3,0),e.moveTo(0,-1e3),e.lineTo(0,1e3),e.stroke(),e.strokeStyle="#00f",e.beginPath(),e.moveTo(t.advance,-1e3),e.lineTo(t.advance,1e3),e.stroke());e.fillStyle="#000",e.beginPath();for(const n of t.contours){e.moveTo(n[0].x1,n[0].y1);for(const t of n)"line"==t.kind?e.lineTo(t.x2,t.y2):"qbezier"==t.kind&&e.quadraticCurveTo(t.x2,t.y2,t.x3,t.y3);e.lineTo(n[0].x1,n[0].y1)}e.fill()}(f,y,g,c)}f.onchange=()=>{let e=new FileReader;e.onload=()=>(function(e){try{l=r.fromBytes(e)}catch(e){throw window.alert("Error loading font!"),e}for(const e of l.warnings)console.warn(e);let t=document.getElementById("selectFontIndex");for(;t.firstChild;)t.removeChild(t.firstChild);for(let e=0;e{d=parseInt(t.value),c()},console.log(l),c()})(new Uint8Array(e.result)),e.readAsArrayBuffer(f.files[0])},document.getElementById("checkboxDrawMetrics").onchange=()=>c(),document.getElementById("checkboxSortUnicode").onchange=()=>c(),document.getElementById("checkboxCustomRender").onchange=()=>c()}]); --------------------------------------------------------------------------------