GitHub Font Preview

A userscript that adds a font file preview

As of 2016-06-23. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          GitHub Font Preview
// @version       1.0.3
// @description   A userscript that adds a font file preview
// @license       https://creativecommons.org/licenses/by-sa/4.0/
// @namespace     http://github.com/Mottie
// @include       https://github.com/*
// @run-at        document-idle
// @grant         GM_addStyle
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_xmlhttpRequest
// @connect       github.com
// @connect       githubusercontent.com
// @require       https://greatest.deepsurf.us/scripts/20469-opentype-js/code/opentypejs.js?version=130870
// @author        Rob Garrison
// ==/UserScript==
/* global GM_addStyle, GM_getValue, GM_setValue, GM_xmlhttpRequest, opentype */
/*jshint unused:true, esnext:true */
(function() {
  'use strict';

  let timer, targets, font,
    busy = false,
    showUnicode = GM_getValue('gfp-show-unicode', false),
    showPoints = GM_getValue('gfp-show-points', true),
    showArrows = GM_getValue('gfp-show-arrows', true),
    currentIndex = 0;

  // supported font types
  const fontExt = /\.(otf|ttf|woff)$/i,

  // canvas colors
  bigGlyphStrokeColor  = '#111111', // (big) stroke color
  bigGlyphFillColor    = '#808080', // (big) fill color
  bigGlyphMarkerColor  = '#f00',    // (big) min & max width marker
  miniGlyphMarkerColor = '#606060', // (mini) glyph index (bottom left corner)
  glyphRulerColor      = '#a0a0a0'; // (mini) min & max width marker & (big) glyph horizontal lines

  function getFont(url) {
    if (url) {
      // add loading indicator
      document.querySelector('.image').innerHTML = '<span class="gfp-loading ghd-invert"></span>';
      GM_xmlhttpRequest({
        method: 'GET',
        url: url,
        responseType: 'arraybuffer',
        onload : function(response) {
          setupFont(response.response);
        }
      });
    }
  }

  function setupFont(data) {
    busy = true;
    let target = document.querySelector('.file .image');
    if (target) {
      try {
        font = opentype.parse(data);
        addHTML(target);
        showErrorMessage('');
        onFontLoaded(font);
      } catch (err) {
      	target.innerHTML = '<h2 class="gfp-message cdel"></h2>';
        showErrorMessage(err.toString());
        if (err.stack) {
          console.error(err.stack);
        }
        throw(err);
      }
    }
    busy = false;
  }

  function addHTML(target) {
    let name = document.querySelector('.final-path').textContent || '';
    target.innerHTML = `
      <div id="gfp-wrapper">
        <span class="gfp-info" id="gfp-font-name">${name}</span>
        <h2 class="gfp-message cdel"></h2>
        <hr>
        <div id="gfp-font-data">
          <div class="gfp-collapsed">Font Header table <a href="https://www.microsoft.com/typography/OTSPEC/head.htm" target="_blank">head</a></div>
          <dl id="gfp-head-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">Horizontal Header table <a href="https://www.microsoft.com/typography/OTSPEC/hhea.htm" target="_blank">hhea</a></div>
          <dl id="gfp-hhea-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">Maximum Profile table <a href="https://www.microsoft.com/typography/OTSPEC/maxp.htm" target="_blank">maxp</a></div>
          <dl id="gfp-maxp-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">Naming table <a href="https://www.microsoft.com/typography/OTSPEC/name.htm" target="_blank">name</a></div>
          <dl id="gfp-name-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">OS/2 and Windows Metrics table <a href="https://www.microsoft.com/typography/OTSPEC/os2.htm" target="_blank">OS/2</a></div>
          <dl id="gfp-os2-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">PostScript table <a href="https://www.microsoft.com/typography/OTSPEC/post.htm" target="_blank">post</a></div>
          <dl id="gfp-post-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">Character To Glyph Index Mapping Table <a href="https://www.microsoft.com/typography/OTSPEC/cmap.htm" target="_blank">cmap</a></div>
          <dl id="gfp-cmap-table"><dt>Undefined</dt></dl>
          <div class="gfp-collapsed">Font Variations table <a href="https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html" target="_blank">fvar</a></div>
          <dl id="gfp-fvar-table"><dt>Undefined</dt></dl>
        </div>
        <hr>
        <div>
          <div>Show unicode: <input class="gfp-show-unicode" type="checkbox"${showUnicode ? ' checked' : ''}></div>
          Glyphs <span id="gfp-pagination"></span>
          <br>
          <div id="gfp-glyph-list-end"></div>
        </div>
        <div style="position: relative">
          <div id="gfp-glyph-display">
            <canvas id="gfp-glyph-bg" class="ghd-invert" width="500" height="500"></canvas>
            <canvas id="gfp-glyph" class="ghd-invert" width="500" height="500"></canvas>
          </div>
          <div id="gfp-glyph-data"></div>
          <div style="clear: both"></div>
        </div>
        <span style="font-size:0.8em">Powered by <a href="https://github.com/nodebox/opentype.js">opentype.js</a></span>
      </div>
    `;
    prepareGlyphList();
    // Add bindings for collapsible font data
    let tableHeaders = document.getElementById('gfp-font-data').getElementsByTagName('div'),
      indx = tableHeaders.length;
    while (indx--) {
      tableHeaders[indx].addEventListener('click', function(e) {
        e.target.classList.toggle('gfp-collapsed');
      }, false);
    }
    addBindings();
  }

  function addBindings() {
    document.querySelector('.gfp-show-unicode').addEventListener('change', function() {
      showUnicode = this.checked;
      GM_setValue('gfp-show-unicode', showUnicode);
      displayGlyphPage(pageSelected);
      return false;
    }, false);
    document.querySelector('#gfp-glyph-data').addEventListener('change', function() {
      showPoints = this.querySelector('.gfp-show-points').checked;
      showArrows = this.querySelector('.gfp-show-arrows').checked;
      GM_setValue('gfp-show-points', showPoints);
      GM_setValue('gfp-show-arrows', showArrows);
      cellSelect();
      return false;
    }, false);
  }

  function init() {
    let name,
      el = document.querySelector('.file .image');
    if (el) {
      name = document.querySelector('.final-path').textContent || '';
      if (name && fontExt.test(name)) {
        getFont(el.querySelector('a').href || '');
      }
    }
  }

  // DOM targets - to detect GitHub dynamic ajax page loading
  targets = document.querySelectorAll([
    '#js-repo-pjax-container',
    '.context-loader-container',
    '[data-pjax-container]'
  ].join(','));

  Array.prototype.forEach.call(targets, function(target) {
    new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        // preform checks before adding code wrap to minimize function calls
        if (!busy && mutation.target === target) {
          clearTimeout(timer);
          timer = setTimeout(init, 200);
        }
      });
    }).observe(target, {
      childList: true,
      subtree: true
    });
  });

  init();

  /* Code modified from http://opentype.js.org/ demos */
  GM_addStyle(`
    #gfp-wrapper { text-align:left; }
    #gfp-wrapper canvas { background-image:none !important; background-color:transparent !important; }
    .gfp-message { position:relative; top:-3px; padding:1px 5px; font-weight:bold; border-radius:2px; display:none; clear:both; }
    #gfp-glyphs { width:950px; }
    .gfp-info { float:right; font-size:11px; color:#999; }
    hr { clear:both; border:none; border-bottom:1px solid #ccc; margin:20px 0 20px 0; padding:0; }
    /* Font Inspector */
    #gfp-font-data div { font-weight:normal; margin:0; cursor:pointer; }
    #gfp-font-data div:before { font-size:85%; content:'▼ '; }
    #gfp-font-data div.gfp-collapsed:before { font-size:85%; content:'► '; }
    #gfp-font-data div.gfp-collapsed + dl { display:none; }
    #gfp-font-data dl { margin-top:0; padding-left:2em; color:#777; }
    #gfp-font-data dt { float:left; }
    #gfp-font-data dd { margin-left: 12em; word-break:break-all; max-height:100px; overflow-y:auto; }
    #gfp-font-data .gfp-langtag { font-size:85%; color:#999; white-space:nowrap; }
    #gfp-font-data .gfp-langname { padding-right:0.5em; }
    #gfp-font-data .gfp-underline { border-bottom:1px solid #555; }
    /* Glyph Inspector */
    #gfp-pagination span { margin:0 0.3em; cursor:pointer; }
    #gfp-pagination span.gfp-page-selected { font-weight:bold; cursor:default; -webkit-filter:brightness(150%); filter:brightness(150%); }
    canvas.gfp-item { float:left; border:solid 1px #a0a0a0; margin-right:-1px; margin-bottom:-1px; cursor:pointer; }
    canvas.gfp-item:hover { opacity:.8; }
    #gfp-glyph-list-end { clear:both; height:20px; }
    #gfp-glyph-display { float:left; border:solid 1px #a0a0a0; position:relative; width:500px; height:500px; }
    #gfp-glyph, #gfp-glyph-bg { position:absolute; top:0; left:0; border:0; }
    #gfp-glyph-data { float:left; margin-left:2em; }
    #gfp-glyph-data dl { margin:0; }
    #gfp-glyph-data dt { float:left; }
    #gfp-glyph-data dd { margin-left:12em; }
    #gfp-glyph-data pre { font-size:11px; }
    pre.gfp-path { margin:0; }
    pre.gfp-contour { margin:0 0 1em 2em; border-bottom:solid 1px #a0a0a0; }
    span.gfp-oncurve { color:blue; }
    span.gfp-offcurve { color:red; }
    .gfp-loading { display:inline-block; margin:0 auto; border-radius:50%; border-width:2px; border-style:solid; border-color: transparent transparent #000 #000; width:30px; height:30px; animation:gfploading .5s infinite linear; }
    @keyframes gfploading { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  `);

  /* Code copied from http://opentype.js.org/font-inspector.html */
  function escapeHtml(unsafe) {
    return unsafe
     .replace(/&/g, '&amp;')
     .replace(/</g, '&lt;')
     .replace(/>/g, '&gt;')
     .replace(/\u0022/g, '&quot;')
     .replace(/\u0027/g, '&#039;');
  }

  function displayNames(names) {
    let indx, property, translations, langs, lang, langIndx, langLen, esclang,
      html = '',
      properties = Object.keys(names),
      len = properties.length;
    for (indx = 0; indx < len; indx++) {
      property = properties[indx];
      html += '<dt>' + escapeHtml(property) + '</dt><dd>';
      translations = names[property];
      langs = Object.keys(translations);
      langLen = langs.length;
      for (langIndx = 0; langIndx < langLen; langIndx++) {
        lang = langs[langIndx];
        esclang = escapeHtml(lang);
        html += '<span class="gfp-langtag">' + esclang +
          '</span> <span class="gfp-langname" lang=' + esclang + '>' +
          escapeHtml(translations[lang]) + '</span> ';
      }
      html += '</dd>';
    }
    document.getElementById('gfp-name-table').innerHTML = html;
  }

  function displayFontData() {
    let html, tablename, table, property, value, element;
    for (tablename in font.tables) {
      if (font.tables.hasOwnProperty(tablename)) {
        table = font.tables[tablename];
        if (tablename === 'name') {
          displayNames(table);
          continue;
        }
        html = '';
        for (property in table) {
          if (table.hasOwnProperty(property)) {
            value = table[property];
            html += '<dt>' + property + '</dt><dd>';
            if (Array.isArray(value) && typeof value[0] === 'object') {
              html += value.map(function(item) {
                return JSON.stringify(item);
              }).join('<br>');
            } else if (typeof value === 'object') {
              html += JSON.stringify(value);
            } else {
              html += value;
            }
            html += '</dd>';
          }
        }
        element = document.getElementById('gfp-' + tablename + '-table');
        if (element) {
          element.innerHTML = html;
        }
      }
    }
  }

  /* Code copied from http://opentype.js.org/glyph-inspector.html */
  const cellCount = 100,
    cellWidth = 62,
    cellHeight = 60,
    cellMarginTop = 1,
    cellMarginBottom = 8,
    cellMarginLeftRight = 1,
    glyphMargin = 5,
    pixelRatio = window.devicePixelRatio || 1,
    arrowLength = 10,
    arrowAperture = 4;

  let pageSelected, fontScale, fontSize, fontBaseline, glyphScale, glyphSize, glyphBaseline;

  function enableHighDPICanvas(canvas) {
    let pixelRatio, oldWidth, oldHeight;
    if (typeof canvas === 'string') {
      canvas = document.getElementById(canvas);
    }
    pixelRatio = window.devicePixelRatio || 1;
    if (pixelRatio === 1) { return; }
    oldWidth = canvas.width;
    oldHeight = canvas.height;
    canvas.width = oldWidth * pixelRatio;
    canvas.height = oldHeight * pixelRatio;
    canvas.style.width = oldWidth + 'px';
    canvas.style.height = oldHeight + 'px';
    canvas.getContext('2d').scale(pixelRatio, pixelRatio);
  }

  function showErrorMessage(message) {
    let el = document.querySelector('.gfp-message');
    el.style.display = (!message || message.trim().length === 0) ? 'none' : 'block';
    el.innerHTML = message;
  }

  function pathCommandToString(cmd) {
    let str = '<strong>' + cmd.type + '</strong> ' +
      ((cmd.x !== undefined) ? 'x=' + cmd.x + ' y=' + cmd.y + ' ' : '') +
      ((cmd.x1 !== undefined) ? 'x1=' + cmd.x1 + ' y1=' + cmd.y1 + ' ' : '') +
      ((cmd.x2 !== undefined) ? 'x2=' + cmd.x2 + ' y2=' + cmd.y2 : '');
    return str;
  }

  function contourToString(contour) {
    return '<pre class="gfp-contour">' + contour.map(function(point) {
      // ".alert.tip" class modified by GitHub Dark style - more readable blue
      // ".cdel" class modified by GitHub Dark style - more readable red
      return '<span class="gfp-' + (point.onCurve ? 'oncurve alert tip' : 'offcurve cdel') +
        '">x=' + point.x + ' y=' + point.y + '</span>';
    }).join('\n') + '</pre>';
  }

  function formatUnicode(unicode) {
    unicode = unicode.toString(16);
    if (unicode.length > 4) {
      return ('000000' + unicode.toUpperCase()).substr(-6);
    } else {
      return ('0000' + unicode.toUpperCase()).substr(-4);
    }
  }

  function displayGlyphData(glyphIndex) {
    let glyph, contours, html,
      container = document.getElementById('gfp-glyph-data'),
      addItem = function(name) {
        return glyph[name] ? `<dt>${name}</dt><dd>${glyph[name]}</dd>` : '';
      };
    if (glyphIndex < 0) {
      container.innerHTML = '';
      return;
    }
    glyph = font.glyphs.get(glyphIndex);
    html = `<dl>
      <dt>Show points</dt>
      <dd><input class="gfp-show-points" type="checkbox"${showPoints ? ' checked' : ''}></dd>
      <dt>Show arrows</dt>
      <dd><input class="gfp-show-arrows" type="checkbox"${showArrows ? ' checked' : ''}></dd>
      <dt>name</dt><dd>${glyph.name}</dd>`;

    if (glyph.unicode) {
      html += '<dt>unicode</dt><dd>' + glyph.unicodes.map(formatUnicode).join(', ') + '</dd>';
    }
    html += addItem('index') +
      addItem('xMin') +
      addItem('xMax') +
      addItem('yMin') +
      addItem('yMax') +
      addItem('advanceWidth') +
      addItem('leftSideBearing') +
      '</dl>';

    if (glyph.numberOfContours > 0) {
      contours = glyph.getContours();
      html += 'contours:<br>' + contours.map(contourToString).join('\n');
    } else if (glyph.isComposite) {
      html += '<br>This composite glyph is a combination of :<ul><li>' +
        glyph.components.map(function(component) {
          return 'glyph ' + component.glyphIndex + ' at dx=' + component.dx +
            ', dy=' + component.dy;
        }).join('</li><li>') + '</li></ul>';
    } else if (glyph.path) {
      html += 'path:<br><pre class="gfp-path">  ' +
        glyph.path.commands.map(pathCommandToString).join('\n  ') + '\n</pre>';
    }
    container.innerHTML = html;
  }

  function drawArrow(ctx, x1, y1, x2, y2) {
    let dx = x2 - x1,
      dy = y2 - y1,
      segmentLength = Math.sqrt(dx * dx + dy * dy),
      unitx = dx / segmentLength,
      unity = dy / segmentLength,
      basex = x2 - arrowLength * unitx,
      basey = y2 - arrowLength * unity,
      normalx = arrowAperture * unity,
      normaly = -arrowAperture * unitx;
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(basex + normalx, basey + normaly);
    ctx.lineTo(basex - normalx, basey - normaly);
    ctx.lineTo(x2, y2);
    ctx.closePath();
    ctx.fill();
  }

  /**
  * This function is Path.prototype.draw with an arrow
  * at the end of each contour.
  */
  function drawPathWithArrows(ctx, path) {
    let indx, cmd, x1, y1, x2, y2,
      arrows = [],
      len = path.commands.length;
    ctx.beginPath();
    for (indx = 0; indx < len; indx++) {
      cmd = path.commands[indx];
      if (cmd.type === 'M') {
        if (x1 !== undefined) {
          arrows.push([ctx, x1, y1, x2, y2]);
        }
        ctx.moveTo(cmd.x, cmd.y);
      } else if (cmd.type === 'L') {
        ctx.lineTo(cmd.x, cmd.y);
        x1 = x2;
        y1 = y2;
      } else if (cmd.type === 'C') {
        ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
        x1 = cmd.x2;
        y1 = cmd.y2;
      } else if (cmd.type === 'Q') {
        ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y);
        x1 = cmd.x1;
        y1 = cmd.y1;
      } else if (cmd.type === 'Z') {
        arrows.push([ctx, x1, y1, x2, y2]);
        ctx.closePath();
      }
      x2 = cmd.x;
      y2 = cmd.y;
    }
    if (path.fill) {
      ctx.fillStyle = path.fill;
      ctx.fill();
    }
    if (path.stroke) {
      ctx.strokeStyle = path.stroke;
      ctx.lineWidth = path.strokeWidth;
      ctx.stroke();
    }
    ctx.fillStyle = bigGlyphStrokeColor;
    if (showArrows) {
      arrows.forEach(function(arrow) {
        drawArrow.apply(null, arrow);
      });
    }
  }

  function displayGlyph(glyphIndex) {
    let glyph, glyphWidth, xmin, xmax, x0, markSize, path,
      canvas = document.getElementById('gfp-glyph'),
      ctx = canvas.getContext('2d'),
      width = canvas.width / pixelRatio,
      height = canvas.height / pixelRatio;
    ctx.clearRect(0, 0, width, height);
    if (glyphIndex < 0) { return; }
    glyph = font.glyphs.get(glyphIndex);
    glyphWidth = glyph.advanceWidth * glyphScale;
    xmin = (width - glyphWidth)/2;
    xmax = (width + glyphWidth)/2;
    x0 = xmin;
    markSize = 10;

    ctx.fillStyle = bigGlyphMarkerColor;
    ctx.fillRect(xmin-markSize+1, glyphBaseline, markSize, 1);
    ctx.fillRect(xmin, glyphBaseline, 1, markSize);
    ctx.fillRect(xmax, glyphBaseline, markSize, 1);
    ctx.fillRect(xmax, glyphBaseline, 1, markSize);
    ctx.textAlign = 'center';
    ctx.fillText('0', xmin, glyphBaseline + markSize + 10);
    ctx.fillText(glyph.advanceWidth, xmax, glyphBaseline + markSize + 10);

    ctx.fillStyle = bigGlyphStrokeColor;
    path = glyph.getPath(x0, glyphBaseline, glyphSize);
    path.fill = bigGlyphFillColor;
    path.stroke = bigGlyphStrokeColor;
    path.strokeWidth = 1.5;
    drawPathWithArrows(ctx, path);
    if (showPoints) {
      glyph.drawPoints(ctx, x0, glyphBaseline, glyphSize);
    }
  }

  function renderGlyphItem(canvas, glyphIndex) {
    const cellMarkSize = 4,
      ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, cellWidth, cellHeight);
    if (glyphIndex >= font.numGlyphs) { return; }

    ctx.fillStyle = miniGlyphMarkerColor;
    ctx.font = '10px sans-serif';
    let glyph = font.glyphs.get(glyphIndex),
      glyphWidth = glyph.advanceWidth * fontScale,
      xmin = (cellWidth - glyphWidth) / 2,
      xmax = (cellWidth + glyphWidth) / 2,
      x0 = xmin;

    ctx.fillText(showUnicode ? glyph.unicodes.map(formatUnicode).join(', ') : glyphIndex, 1, cellHeight-1);

    ctx.fillStyle = glyphRulerColor;
    ctx.fillRect(xmin-cellMarkSize+1, fontBaseline, cellMarkSize, 1);
    ctx.fillRect(xmin, fontBaseline, 1, cellMarkSize);
    ctx.fillRect(xmax, fontBaseline, cellMarkSize, 1);
    ctx.fillRect(xmax, fontBaseline, 1, cellMarkSize);

    ctx.fillStyle = '#000000';
    glyph.draw(ctx, x0, fontBaseline, fontSize);
  }

  function displayGlyphPage(pageNum) {
    pageSelected = pageNum;
    document.getElementById('gfp-p' + pageNum).className = 'gfp-page-selected';
    let indx,
      firstGlyph = pageNum * cellCount;
    for (indx = 0; indx < cellCount; indx++) {
      renderGlyphItem(document.getElementById('gfp-g' + indx), firstGlyph + indx);
    }
  }

  function pageSelect(event) {
    document.getElementsByClassName('gfp-page-selected')[0].className = 'text-blue';
    displayGlyphPage((event.target.id || '').replace('gfp-p', ''));
  }

  function initGlyphDisplay() {
    let glyphBgCanvas = document.getElementById('gfp-glyph-bg'),
      w = glyphBgCanvas.width / pixelRatio,
      h = glyphBgCanvas.height / pixelRatio,
      glyphW = w - glyphMargin*2,
      glyphH = h - glyphMargin*2,
      head = font.tables.head,
      maxHeight = head.yMax - head.yMin,
      ctx = glyphBgCanvas.getContext('2d');

    glyphScale = Math.min(glyphW/(head.xMax - head.xMin), glyphH/maxHeight);
    glyphSize = glyphScale * font.unitsPerEm;
    glyphBaseline = glyphMargin + glyphH * head.yMax / maxHeight;

    function hline(text, yunits) {
      let ypx = glyphBaseline - yunits * glyphScale;
      ctx.fillText(text, 2, ypx + 3);
      ctx.fillRect(80, ypx, w, 1);
    }

    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = glyphRulerColor;
    hline('Baseline', 0);
    hline('yMax', font.tables.head.yMax);
    hline('yMin', font.tables.head.yMin);
    hline('Ascender', font.tables.hhea.ascender);
    hline('Descender', font.tables.hhea.descender);
    hline('Typo Ascender', font.tables.os2.sTypoAscender);
    hline('Typo Descender', font.tables.os2.sTypoDescender);
  }

  function onFontLoaded(font) {
    let indx, link, lastIndex,
      w = cellWidth - cellMarginLeftRight * 2,
      h = cellHeight - cellMarginTop - cellMarginBottom,
      head = font.tables.head,
      maxHeight = head.yMax - head.yMin,
      pagination = document.getElementById('gfp-pagination'),
      fragment = document.createDocumentFragment(),
      numPages = Math.ceil(font.numGlyphs / cellCount);

    fontScale = Math.min(w/(head.xMax - head.xMin), h/maxHeight);
    fontSize = fontScale * font.unitsPerEm;
    fontBaseline = cellMarginTop + h * head.yMax / maxHeight;
    pagination.innerHTML = '';

    for (indx = 0; indx < numPages; indx++) {
      link = document.createElement('span');
      lastIndex = Math.min(font.numGlyphs - 1, (indx + 1) * cellCount - 1);
      link.textContent = indx * cellCount + '-' + lastIndex;
      link.id = 'gfp-p' + indx;
      link.className = 'text-blue';
      link.addEventListener('click', pageSelect, false);
      fragment.appendChild(link);
      // A white space allows to break very long lines into multiple lines.
      // This is needed for fonts with thousands of glyphs.
      fragment.appendChild(document.createTextNode(' '));
    }
    pagination.appendChild(fragment);

    displayFontData();
    initGlyphDisplay();
    displayGlyphPage(0);
    displayGlyph(-1);
    displayGlyphData(-1);
  }

  function cellSelect(event) {
    if (!font) { return; }
    let firstGlyphIndex = pageSelected * cellCount,
      cellIndex = event ? +event.target.id.replace('gfp-g', '') : currentIndex,
      glyphIndex = firstGlyphIndex + cellIndex;
    currentIndex = cellIndex;
    if (glyphIndex < font.numGlyphs) {
      displayGlyph(glyphIndex);
      displayGlyphData(glyphIndex);
    }
  }

  function prepareGlyphList() {
    let indx, canvas,
      marker = document.getElementById('gfp-glyph-list-end'),
    parent = marker.parentElement;
    for (indx = 0; indx < cellCount; indx++) {
      canvas = document.createElement('canvas');
      canvas.width = cellWidth;
      canvas.height = cellHeight;
      canvas.className = 'gfp-item ghd-invert';
      canvas.id = 'gfp-g' + indx;
      canvas.addEventListener('click', cellSelect, false);
      enableHighDPICanvas(canvas);
      parent.insertBefore(canvas, marker);
    }
  }

})();