glyph-inspector.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>opentype.js glyph inspector</title> 5 <meta name="description" content="A JavaScript library to manipulate the letterforms of text from the browser or node.js."> 6 <meta charset="utf-8"> 7 <link rel="stylesheet" href="site.css"> 8 <script src="dist/opentype.js"></script> 9 </head> 10 <body> 11 <div class="header"> 12 <div class="container"> 13 <h1>opentype.js</h1> 14 <nav> 15 <a href="index.html">Home</a> 16 <a href="font-inspector.html">Font Inspector</a> 17 <a href="glyph-inspector.html">Glyph Inspector</a> 18 </nav> 19 <nav class="right"> 20 <a class="github" href="https://github.com/nodebox/opentype.js">Fork me on GitHub</a> 21 <a class="gitter" href="https://gitter.im/nodebox/opentype.js">Chat on Gitter</a> 22 </nav> 23 </div> 24 </div> 25 26 <div class="container"> 27 28 <div class="explain"> 29 <h1>Glyph Inspector</h1> 30 <small>opentype.js is an OpenType and TrueType font parser. Here you can inspect the raw glyph data.</small> 31 </div> 32 33 <input id="file" type="file"> 34 <span class="info" id="font-name">Roboto-Black</span> 35 <div id="message"></div> 36 37 <hr> 38 39 <div> 40 Glyphs <span id="pagination"></span> 41 <br> 42 <div id="glyph-list-end"></div> 43 </div> 44 <div style="position: relative"> 45 <div id="glyph-display"> 46 <canvas id="glyph-bg" width="500" height="500"></canvas> 47 <canvas id="glyph" width="500" height="500"></canvas> 48 </div> 49 <div id="glyph-data"></div> 50 <div style="clear: both"></div> 51 </div> 52 53 <hr> 54 55 <div class="explain"> 56 <h1>Free Software</h1> 57 <p>opentype.js is available on <a href="https://github.com/nodebox/opentype.js">GitHub</a> under the <a href="https://raw.github.com/nodebox/opentype.js/master/LICENSE">MIT License</a>.</p> 58 <p>Copyright © 2017 Frederik De Bleser.</p> 59 </div> 60 61 <hr> 62 </div> 63 64 65 <script> 66 var cellCount = 100, 67 cellWidth = 44, 68 cellHeight = 40, 69 cellMarginTop = 1, 70 cellMarginBottom = 8, 71 cellMarginLeftRight = 1, 72 glyphMargin = 5, 73 pixelRatio = window.devicePixelRatio || 1; 74 75 var pageSelected, font, fontScale, fontSize, fontBaseline, glyphScale, glyphSize, glyphBaseline; 76 77 function enableHighDPICanvas(canvas) { 78 if (typeof canvas === 'string') { 79 canvas = document.getElementById(canvas); 80 } 81 var pixelRatio = window.devicePixelRatio || 1; 82 if (pixelRatio === 1) return; 83 var oldWidth = canvas.width; 84 var oldHeight = canvas.height; 85 canvas.width = oldWidth * pixelRatio; 86 canvas.height = oldHeight * pixelRatio; 87 canvas.style.width = oldWidth + 'px'; 88 canvas.style.height = oldHeight + 'px'; 89 canvas.getContext('2d').scale(pixelRatio, pixelRatio); 90 } 91 92 function showErrorMessage(message) { 93 var el = document.getElementById('message'); 94 if (!message || message.trim().length === 0) { 95 el.style.display = 'none'; 96 } else { 97 el.style.display = 'block'; 98 } 99 el.innerHTML = message; 100 } 101 102 function pathCommandToString(cmd) { 103 var str = '<strong>' + cmd.type + '</strong> ' + 104 ((cmd.x !== undefined) ? 'x='+cmd.x+' y='+cmd.y+' ' : '') + 105 ((cmd.x1 !== undefined) ? 'x1='+cmd.x1+' y1='+cmd.y1+' ' : '') + 106 ((cmd.x2 !== undefined) ? 'x2='+cmd.x2+' y2='+cmd.y2 : ''); 107 return str; 108 } 109 110 function contourToString(contour) { 111 return '<pre class="contour">' + contour.map(function(point) { 112 return '<span class="' + (point.onCurve ? 'on' : 'off') + 'curve">x=' + point.x + ' y=' + point.y + '</span>'; 113 }).join('\n') + '</pre>'; 114 } 115 116 function formatUnicode(unicode) { 117 unicode = unicode.toString(16); 118 if (unicode.length > 4) { 119 return ("000000" + unicode.toUpperCase()).substr(-6) 120 } else { 121 return ("0000" + unicode.toUpperCase()).substr(-4) 122 } 123 } 124 125 function displayGlyphData(glyphIndex) { 126 var container = document.getElementById('glyph-data'); 127 if (glyphIndex < 0) { 128 container.innerHTML = ''; 129 return; 130 } 131 var glyph = font.glyphs.get(glyphIndex), 132 html = '<dl>'; 133 html += '<dt>name</dt><dd>'+glyph.name+'</dd>'; 134 135 if (glyph.unicodes.length > 0) { 136 html += '<dt>unicode</dt><dd>'+ glyph.unicodes.map(formatUnicode).join(', ') +'</dd>'; 137 } 138 html += '<dt>index</dt><dd>'+glyph.index+'</dd>'; 139 140 if (glyph.xMin !== 0 || glyph.xMax !== 0 || glyph.yMin !== 0 || glyph.yMax !== 0) { 141 html += '<dt>xMin</dt><dd>'+glyph.xMin+'</dd>' + 142 '<dt>xMax</dt><dd>'+glyph.xMax+'</dd>' + 143 '<dt>yMin</dt><dd>'+glyph.yMin+'</dd>' + 144 '<dt>yMax</dt><dd>'+glyph.yMax+'</dd>'; 145 } 146 html += '<dt>advanceWidth</dt><dd>'+glyph.advanceWidth+'</dd>'; 147 if(glyph.leftSideBearing !== undefined) { 148 html += '<dt>leftSideBearing</dt><dd>'+glyph.leftSideBearing+'</dd>'; 149 } 150 html += '</dl>'; 151 if (glyph.numberOfContours > 0) { 152 var contours = glyph.getContours(); 153 html += 'contours:<div id="glyph-contours">' + contours.map(contourToString).join('\n') + '</div>'; 154 } else if (glyph.isComposite) { 155 html += '<br>This composite glyph is a combination of :<ul><li>' + 156 glyph.components.map(function(component) { 157 if (component.matchedPoints === undefined) { 158 return 'glyph '+component.glyphIndex+' at dx='+component.dx+', dy='+component.dy; 159 } else { 160 return 'glyph '+component.glyphIndex+' at matchedPoints=['+component.matchedPoints+']'; 161 } 162 }).join('</li><li>') + '</li></ul>'; 163 } else if (glyph.path) { 164 html += 'path:<br><pre> ' + glyph.path.commands.map(pathCommandToString).join('\n ') + '\n</pre>'; 165 } 166 container.innerHTML = html; 167 } 168 169 var arrowLength = 10, 170 arrowAperture = 4; 171 172 function drawArrow(ctx, x1, y1, x2, y2) { 173 var dx = x2 - x1, 174 dy = y2 - y1, 175 segmentLength = Math.sqrt(dx*dx + dy*dy), 176 unitx = dx / segmentLength, 177 unity = dy / segmentLength, 178 basex = x2 - arrowLength * unitx, 179 basey = y2 - arrowLength * unity, 180 normalx = arrowAperture * unity, 181 normaly = -arrowAperture * unitx; 182 ctx.beginPath(); 183 ctx.moveTo(x2, y2); 184 ctx.lineTo(basex + normalx, basey + normaly); 185 ctx.lineTo(basex - normalx, basey - normaly); 186 ctx.lineTo(x2, y2); 187 ctx.closePath(); 188 ctx.fill(); 189 } 190 191 /** 192 * This function is Path.prototype.draw with an arrow 193 * at the end of each contour. 194 */ 195 function drawPathWithArrows(ctx, path) { 196 var i, cmd, x1, y1, x2, y2; 197 var arrows = []; 198 ctx.beginPath(); 199 for (i = 0; i < path.commands.length; i += 1) { 200 cmd = path.commands[i]; 201 if (cmd.type === 'M') { 202 if(x1 !== undefined) { 203 arrows.push([ctx, x1, y1, x2, y2]); 204 } 205 ctx.moveTo(cmd.x, cmd.y); 206 } else if (cmd.type === 'L') { 207 ctx.lineTo(cmd.x, cmd.y); 208 x1 = x2; 209 y1 = y2; 210 } else if (cmd.type === 'C') { 211 ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); 212 x1 = cmd.x2; 213 y1 = cmd.y2; 214 } else if (cmd.type === 'Q') { 215 ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y); 216 x1 = cmd.x1; 217 y1 = cmd.y1; 218 } else if (cmd.type === 'Z') { 219 arrows.push([ctx, x1, y1, x2, y2]); 220 ctx.closePath(); 221 } 222 x2 = cmd.x; 223 y2 = cmd.y; 224 } 225 if (path.fill) { 226 ctx.fillStyle = path.fill; 227 ctx.fill(); 228 } 229 if (path.stroke) { 230 ctx.strokeStyle = path.stroke; 231 ctx.lineWidth = path.strokeWidth; 232 ctx.stroke(); 233 } 234 ctx.fillStyle = '#000000'; 235 arrows.forEach(function(arrow) { 236 drawArrow.apply(null, arrow); 237 }); 238 } 239 240 function displayGlyph(glyphIndex) { 241 var canvas = document.getElementById('glyph'), 242 ctx = canvas.getContext('2d'), 243 width = canvas.width / pixelRatio, 244 height = canvas.height / pixelRatio; 245 ctx.clearRect(0, 0, width, height); 246 if(glyphIndex < 0) return; 247 var glyph = font.glyphs.get(glyphIndex), 248 glyphWidth = glyph.advanceWidth * glyphScale, 249 xmin = (width - glyphWidth)/2, 250 xmax = (width + glyphWidth)/2, 251 x0 = xmin, 252 markSize = 10; 253 254 ctx.fillStyle = '#606060'; 255 ctx.fillRect(xmin-markSize+1, glyphBaseline, markSize, 1); 256 ctx.fillRect(xmin, glyphBaseline, 1, markSize); 257 ctx.fillRect(xmax, glyphBaseline, markSize, 1); 258 ctx.fillRect(xmax, glyphBaseline, 1, markSize); 259 ctx.textAlign = 'center'; 260 ctx.fillText('0', xmin, glyphBaseline+markSize+10); 261 ctx.fillText(glyph.advanceWidth, xmax, glyphBaseline+markSize+10); 262 263 ctx.fillStyle = '#000000'; 264 var path = glyph.getPath(x0, glyphBaseline, glyphSize); 265 path.fill = '#808080'; 266 path.stroke = '#000000'; 267 path.strokeWidth = 1.5; 268 drawPathWithArrows(ctx, path); 269 glyph.drawPoints(ctx, x0, glyphBaseline, glyphSize); 270 } 271 272 function renderGlyphItem(canvas, glyphIndex) { 273 var cellMarkSize = 4; 274 var ctx = canvas.getContext('2d'); 275 ctx.clearRect(0, 0, cellWidth, cellHeight); 276 if (glyphIndex >= font.numGlyphs) return; 277 278 ctx.fillStyle = '#606060'; 279 ctx.font = '9px sans-serif'; 280 ctx.fillText(glyphIndex, 1, cellHeight-1); 281 var glyph = font.glyphs.get(glyphIndex), 282 glyphWidth = glyph.advanceWidth * fontScale, 283 xmin = (cellWidth - glyphWidth)/2, 284 xmax = (cellWidth + glyphWidth)/2, 285 x0 = xmin; 286 287 ctx.fillStyle = '#a0a0a0'; 288 ctx.fillRect(xmin-cellMarkSize+1, fontBaseline, cellMarkSize, 1); 289 ctx.fillRect(xmin, fontBaseline, 1, cellMarkSize); 290 ctx.fillRect(xmax, fontBaseline, cellMarkSize, 1); 291 ctx.fillRect(xmax, fontBaseline, 1, cellMarkSize); 292 293 ctx.fillStyle = '#000000'; 294 glyph.draw(ctx, x0, fontBaseline, fontSize); 295 } 296 297 function displayGlyphPage(pageNum) { 298 pageSelected = pageNum; 299 document.getElementById('p'+pageNum).className = 'page-selected'; 300 var firstGlyph = pageNum * cellCount; 301 for(var i = 0; i < cellCount; i++) { 302 renderGlyphItem(document.getElementById('g'+i), firstGlyph+i); 303 } 304 } 305 306 function pageSelect(event) { 307 document.getElementsByClassName('page-selected')[0].className = ''; 308 displayGlyphPage(+event.target.id.substr(1)); 309 } 310 311 function initGlyphDisplay() { 312 var glyphBgCanvas = document.getElementById('glyph-bg'), 313 w = glyphBgCanvas.width / pixelRatio, 314 h = glyphBgCanvas.height / pixelRatio, 315 glyphW = w - glyphMargin*2, 316 glyphH = h - glyphMargin*2, 317 head = font.tables.head, 318 maxHeight = head.yMax - head.yMin, 319 ctx = glyphBgCanvas.getContext('2d'); 320 321 glyphScale = Math.min(glyphW/(head.xMax - head.xMin), glyphH/maxHeight); 322 glyphSize = glyphScale * font.unitsPerEm; 323 glyphBaseline = glyphMargin + glyphH * head.yMax / maxHeight; 324 325 function hline(text, yunits) { 326 ypx = glyphBaseline - yunits * glyphScale; 327 ctx.fillText(text, 2, ypx+3); 328 ctx.fillRect(80, ypx, w, 1); 329 } 330 331 ctx.clearRect(0, 0, w, h); 332 ctx.fillStyle = '#a0a0a0'; 333 hline('Baseline', 0); 334 hline('yMax', font.tables.head.yMax); 335 hline('yMin', font.tables.head.yMin); 336 hline('Ascender', font.tables.hhea.ascender); 337 hline('Descender', font.tables.hhea.descender); 338 hline('Typo Ascender', font.tables.os2.sTypoAscender); 339 hline('Typo Descender', font.tables.os2.sTypoDescender); 340 } 341 342 function onFontLoaded(font) { 343 window.font = font; 344 345 var w = cellWidth - cellMarginLeftRight * 2, 346 h = cellHeight - cellMarginTop - cellMarginBottom, 347 head = font.tables.head, 348 maxHeight = head.yMax - head.yMin; 349 fontScale = Math.min(w/(head.xMax - head.xMin), h/maxHeight); 350 fontSize = fontScale * font.unitsPerEm; 351 fontBaseline = cellMarginTop + h * head.yMax / maxHeight; 352 353 var pagination = document.getElementById("pagination"); 354 pagination.innerHTML = ''; 355 var fragment = document.createDocumentFragment(); 356 var numPages = Math.ceil(font.numGlyphs / cellCount); 357 for(var i = 0; i < numPages; i++) { 358 var link = document.createElement('span'); 359 var lastIndex = Math.min(font.numGlyphs-1, (i+1)*cellCount-1); 360 link.textContent = i*cellCount + '-' + lastIndex; 361 link.id = 'p' + i; 362 link.addEventListener('click', pageSelect, false); 363 fragment.appendChild(link); 364 // A white space allows to break very long lines into multiple lines. 365 // This is needed for fonts with thousands of glyphs. 366 fragment.appendChild(document.createTextNode(' ')); 367 } 368 pagination.appendChild(fragment); 369 370 initGlyphDisplay(); 371 displayGlyphPage(0); 372 displayGlyph(-1); 373 displayGlyphData(-1); 374 } 375 376 function onReadFile(e) { 377 document.getElementById('font-name').innerHTML = ''; 378 var file = e.target.files[0]; 379 var reader = new FileReader(); 380 reader.onload = function(e) { 381 try { 382 font = opentype.parse(e.target.result); 383 showErrorMessage(''); 384 onFontLoaded(font); 385 } catch (err) { 386 showErrorMessage(err.toString()); 387 if (err.stack) console.log(err.stack); 388 throw(err); 389 } 390 }; 391 reader.onerror = function(err) { 392 showErrorMessage(err.toString()); 393 }; 394 395 reader.readAsArrayBuffer(file); 396 } 397 398 function cellSelect(event) { 399 if (!font) return; 400 var firstGlyphIndex = pageSelected*cellCount, 401 cellIndex = +event.target.id.substr(1), 402 glyphIndex = firstGlyphIndex + cellIndex; 403 if (glyphIndex < font.numGlyphs) { 404 displayGlyph(glyphIndex); 405 displayGlyphData(glyphIndex); 406 } 407 } 408 409 function prepareGlyphList() { 410 var marker = document.getElementById('glyph-list-end'), 411 parent = marker.parentElement; 412 for(var i = 0; i < cellCount; i++) { 413 var canvas = document.createElement('canvas'); 414 canvas.width = cellWidth; 415 canvas.height = cellHeight; 416 canvas.className = 'item'; 417 canvas.id = 'g'+i; 418 canvas.addEventListener('click', cellSelect, false); 419 enableHighDPICanvas(canvas); 420 parent.insertBefore(canvas, marker); 421 } 422 } 423 424 var fontFileName = 'fonts/Roboto-Black.ttf'; 425 document.getElementById('font-name').innerHTML = fontFileName.split('/')[1]; 426 427 var fileButton = document.getElementById('file'); 428 fileButton.addEventListener('change', onReadFile, false); 429 430 enableHighDPICanvas('glyph-bg'); 431 enableHighDPICanvas('glyph'); 432 433 prepareGlyphList(); 434 opentype.load(fontFileName, function(err, font) { 435 var amount, glyph, ctx, x, y, fontSize; 436 if (err) { 437 showErrorMessage(err.toString()); 438 return; 439 } 440 onFontLoaded(font); 441 }); 442 </script> 443 </body> 444 </html>