satie
Version:
A sheet music renderer for the web
224 lines (223 loc) • 8.18 kB
JavaScript
/**
* This file is part of Satie music engraver <https://github.com/jnetterf/satie>.
* Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present.
*
* Satie is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Satie is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Satie. If not, see <http://www.gnu.org/licenses/>.
*/
;
var opentype_js_1 = require("opentype.js");
var lodash_1 = require("lodash");
var IS_BROWSER = "browser" in process;
var NO_PATH_DATA = {};
/*---- PRIVATE ------------------------------------------------------------------------*/
var State;
(function (State) {
State.fonts = {};
State.cbs = [];
State.remaining = 0;
State.canvasContext = IS_BROWSER ?
document.createElement("canvas").getContext("2d") :
null;
State.root = IS_BROWSER ?
location.protocol + "//" + location.host + "/vendor/" :
"./vendor/";
})(State || (State = {}));
function getFullName(name, style) {
name = name.toLowerCase();
style = style && style.toLowerCase();
return "" + name + (style ? "_" + style : "");
}
function loadFont(name, url, style, full) {
++State.remaining;
var fullName = getFullName(name, style);
url = getNativeURL(url);
if (!full && IS_BROWSER) {
var styleSheet = document.createElement("style");
styleSheet.appendChild(document.createTextNode("@font-face{\n font-family: " + name + ";\n src: url(" + url + ") format('truetype');\n " + (style && style.toLowerCase() === "bold" ? "font-weight: bold;" : "") + "\n }"));
document.head.appendChild(styleSheet);
State.fonts[fullName] = State.fonts[fullName] || NO_PATH_DATA;
goOn();
}
else {
(IS_BROWSER ? loadFromUrl : loadFromFile)(url, function (err, buffer) {
if (err) {
return goOn(err);
}
var font = opentype_js_1.parse(buffer);
State.fonts[fullName] = font;
if (IS_BROWSER) {
var styleSheet = document.styleSheets[0];
var fontFaceStyle = "@font-face{\n font-family: " + name + ";\n src: url(data:font/truetype;charset=utf-8;base64," + toBase64(buffer) + ") format('truetype');\n " + (style && style.toLowerCase() === "bold" ? "font-weight: bold;" : "") + "\n }";
styleSheet.insertRule(fontFaceStyle, 0);
}
goOn();
});
}
function goOn(err) {
--State.remaining;
if (!State.remaining) {
lodash_1.forEach(State.cbs, function (cb) { return cb(State.err); });
State.cbs = [];
}
}
}
/*---- SUPPORT ------------------------------------------------------------------------*/
function toArrayBuffer(buffer) {
var arrayBuffer = new ArrayBuffer(buffer.length);
var data = new Uint8Array(arrayBuffer);
for (var i = 0; i < buffer.length; i += 1) {
data[i] = buffer[i];
}
return arrayBuffer;
}
function loadFromFile(path, callback) {
var fs = require("fs");
fs.readFile(path, function (err, buffer) {
if (err) {
return callback(err);
}
callback(null, toArrayBuffer(buffer));
});
}
function loadFromUrl(url, callback) {
var request = new XMLHttpRequest();
request.open("get", url, true);
request.responseType = "arraybuffer";
request.onload = function () {
if (request.status !== 200) {
return callback(new Error("Font could not be loaded: " + request.statusText));
}
return callback(null, request.response);
};
request.send();
}
function getNativeURL(url) {
return url.replace("root://", State.root);
}
var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function toBase64(buffer) {
var bytes = new Uint8Array(buffer);
var len = bytes.length, base64 = "";
for (var i = 0; i < len; i += 3) {
/* tslint:disable */
base64 += CHARS[bytes[i] >> 2];
base64 += CHARS[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += CHARS[bytes[i + 2] & 63];
}
if ((len % 3) === 2) {
base64 = base64.substring(0, base64.length - 1) + "=";
}
else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + "==";
}
return base64;
}
/*---- PUBLIC -------------------------------------------------------------------------*/
function requireFont(name, url, style, full) {
var fullName = getFullName(name, style);
if (full && State.fonts[fullName] === NO_PATH_DATA) {
delete State.fonts[fullName];
}
if (!(fullName in State.fonts)) {
State.fonts[fullName] = null; // Indicate it's pending
loadFont(name, url, style, full);
}
}
exports.requireFont = requireFont;
function setRoot(root) {
State.root = root;
}
exports.setRoot = setRoot;
function markPreloaded(name, style) {
State.fonts[getFullName(name, style)] = NO_PATH_DATA;
}
exports.markPreloaded = markPreloaded;
function whenReady(cb) {
if (!State.remaining) {
cb();
return;
}
State.cbs.push(cb);
}
exports.whenReady = whenReady;
function getTextBB(name, text, fontSize, style) {
var fullName = getFullName(name, style);
var font = State.fonts[fullName];
if (State.canvasContext && font === NO_PATH_DATA) {
State.canvasContext.font = (style || "") + " " + fontSize + "px " + name;
// We want to be consistent between web browsers. Many browsers only support measuring
// width, so even if we are in Chrome and have better information, we ignore that.
// Of course that this information is wrong, but it's good enough to place text.
return {
bottom: fontSize,
left: -fontSize / 18,
right: State.canvasContext.measureText(text).width,
top: -4 * fontSize / 18
};
}
if (font === NO_PATH_DATA) {
// TODO: get width by canvas if this is the browser
console.warn(fullName + " was loaded without path data");
return {
bottom: 1,
left: 0,
right: 1,
top: 0
};
}
if (!font) {
console.warn(fullName + " is not loaded");
return {
bottom: 1,
left: 0,
right: 1,
top: 0
};
}
var minX = 10000;
var minY = 10000;
var maxX = 0;
var maxY = 0;
font.forEachGlyph(text, 0, 0, fontSize, { kerning: true }, function (glyph, x, y, fontSize) {
var scale = 1 / font.unitsPerEm * fontSize;
minX = Math.min(x, minX);
maxX = Math.max(x, maxX);
minY = Math.min(y + glyph.yMin * scale, minY);
maxY = Math.max(y + glyph.yMax * scale, maxY);
});
return {
bottom: maxY,
left: minX,
right: maxX,
top: minY
};
}
exports.getTextBB = getTextBB;
exports.toPathData = lodash_1.memoize(function (name, text, x, y, fontSize, style) {
var fullName = getFullName(name, style);
var font = State.fonts[fullName];
if (!font) {
console.warn(fullName + " is not loaded");
return "";
}
if (font === NO_PATH_DATA) {
console.warn(fullName + " was loaded without path data");
return "";
}
return font.getPath(text, x, y, fontSize, { kerning: true }).toPathData(3);
}, resolvePDKey);
function resolvePDKey(name, text, x, y, fontSize, style) {
return name + "_" + text + "_" + x + "_" + y + "_" + fontSize + "_" + (style || "");
}