// ==UserScript== // @name GitHub Font Preview // @version 1.0.26 // @description A userscript that adds a font file preview // @license MIT // @author Rob Garrison // @namespace https://github.com/Mottie // @match 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://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163 // @require https://greasyfork.org/scripts/20469-opentype-js/code/opentypejs.js?version=130870 // @icon https://github.githubassets.com/pinned-octocat.svg // @updateURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-font-preview.user.js // @downloadURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-font-preview.user.js // @supportURL https://github.com/Mottie/GitHub-userscripts/issues // ==/UserScript== /* global opentype */ (() => { "use strict"; let font; let showUnicode = GM_getValue("gfp-show-unicode", false); let showPoints = GM_getValue("gfp-show-points", true); let showArrows = GM_getValue("gfp-show-arrows", true); let currentIndex = 0; // supported font types const fontExt = /\.(otf|ttf|woff)$/i; // canvas colors const glyphFillColor = "#808080"; // (big) (mini) fill color const bigGlyphStrokeColor = "#111111"; // (big) stroke color const bigGlyphMarkerColor = "#f00"; // (big) min & max width marker const miniGlyphMarkerColor = "#606060"; // (mini) glyph index (bottom left corner) const glyphRulerColor = "#a0a0a0"; // (mini) min & max width marker & (big) glyph horizontal lines function startLoad() { const block = $(".blob-wrapper a[href*='?raw=true']"); const body = block && block.closest(".Box-body"); if (body) { body.classList.add("ghfp-body"); body.innerHTML = ""; } return block && block.href; } function getFont() { const url = startLoad(); if (url) { // add loading indicator GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", onload: response => { setupFont(response.response); } }); } } function setupFont(data) { const block = $(".ghfp-body"); const el = $(".final-path"); if (block && el) { try { font = opentype.parse(data); addHTML(block, el); showErrorMessage(""); onFontLoaded(font); } catch (err) { block.innerHTML = "
"; showErrorMessage(err.toString()); if (err.stack) { console.error(err.stack); } throw (err); } } } function addHTML(block, el) { let name = el.textContent || ""; block.innerHTML = `' + contour.map(point => {
return 'x=' + point.x + ' y=' + point.y + '';
}).join('\n') + '
';
}
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 = name => {
return glyph[name] ? `' + glyph.path.commands.map(pathCommandToString).join('\n ') + '\n'; } 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(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 = glyphFillColor; 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'; let path = glyph.getPath(x0, fontBaseline, fontSize); path.fill = glyphFillColor; path.draw(ctx); } function displayGlyphPage(pageNum) { pageSelected = pageNum; const last = $('.gfp-page-selected'); if (last) last.className = ''; 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) { 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('a'); lastIndex = Math.min(font.numGlyphs - 1, (indx + 1) * cellCount - 1); link.textContent = indx * cellCount + '-' + lastIndex; link.id = 'gfp-p' + indx; 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); } } /* eslint-enable */ })();
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: