Skip to content

Commit

Permalink
Implement TextSVGContext, now VexFlow can run without web browser
Browse files Browse the repository at this point in the history
TextSVGContext uses opentype.js lib to replace getBBox which parses font file and draws text to svg path,
And get bounding box using svg-path-bounding-box.
As a final step, it generates svg text using react.
Now generating svg files runs on node.js directly, SlimerJS dependency removed
  • Loading branch information
panarch committed May 5, 2016
1 parent 8ccf963 commit c6727ef
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 39 deletions.
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ module.exports = function(grunt) {
"src/renderer.js",
"src/raphaelcontext.js",
"src/svgcontext.js",
"src/textsvgcontext.js",
"src/canvascontext.js",
"src/stavebarline.js",
"src/stavehairpin.js",
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,20 @@
"grunt-release": "^0.7.0",
"jquery": "^2.1.1",
"jscs": "^2.3.3",
"mkdirp": "^0.5.1",
"opentype.js": "^0.6.2",
"qunit": "^0.7.5",
"raphael": "^2.1.0",
"slimerjs": "^0.9.6"
"react": "^15.0.2",
"react-dom": "^15.0.2",
"svg-path-bounding-box": "^1.0.4"
},
"scripts": {
"start": "grunt stage",
"lint": "grunt jshint",
"qunit": "grunt test",
"generate:current": "slimerjs ./tools/generate_svg_images.js ../build ./build/images/current",
"generate:blessed": "slimerjs ./tools/generate_svg_images.js ../releases ./build/images/blessed",
"generate:current": "node ./tools/generate_svg_images.js ../build ./build/images/current",
"generate:blessed": "node ./tools/generate_svg_images.js ../releases ./build/images/blessed",
"generate": "npm run generate:current && npm run generate:blessed",
"diff": "./tools/visual_regression.sh",
"test": "npm run lint && npm run qunit && npm run generate && npm run diff"
Expand Down
9 changes: 7 additions & 2 deletions src/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ Vex.Flow.Renderer = (function() {
width, height, background);
};

Renderer.getTextSVGContext = function(options, width, height, background) {
var ctx = new Vex.Flow.TextSVGContext(options);
ctx.resize(width, height);

Renderer.lastContext = ctx;
return ctx;
};

Renderer.bolsterCanvasContext = function(ctx) {
if (Renderer.USE_CANVAS_PROXY) {
Expand Down Expand Up @@ -165,5 +172,3 @@ Vex.Flow.Renderer = (function() {

return Renderer;
}());


5 changes: 3 additions & 2 deletions src/svgcontext.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Vex.Flow.SVGContext = (function() {
this.svgNS = "http://www.w3.org/2000/svg";
var svg = this.create("svg");
// Add it to the canvas:
this.element.appendChild(svg);
this.parent = this.element;
this.add(svg);

// Point to it:
this.svg = svg;
Expand Down Expand Up @@ -78,7 +79,7 @@ Vex.Flow.SVGContext = (function() {
openGroup: function(cls, id, attrs) {
var group = this.create("g");
this.groups.push(group);
this.parent.appendChild(group);
this.add(group);
this.parent = group;
if (cls) group.setAttribute("class", SVGContext.addPrefix(cls));
if (id) group.setAttribute("id", SVGContext.addPrefix(id));
Expand Down
122 changes: 122 additions & 0 deletions src/textsvgcontext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Vex Flow
//
// @copyright Mohit Muthanna 2016
// @author Taehoon Moon 2016

/** @constructor */
Vex.Flow.TextSVGContext = (function() {
function TextSVGContext(options) {
if (arguments.length > 0) this.init(options);
}

Vex.Inherit(TextSVGContext, Vex.Flow.SVGContext, {
init: function(options) {
this.React = options.React;
this.ReactDOMServer = options.ReactDOMServer;
this.fontObj = options.font;
this.getBoundingBox = options.getBoundingBox;

TextSVGContext.superclass.init.call(this, this.create('div'));
},

create: function(svgElementType) {
var props = {
style: {}
};

// Add xmlns to root
if (svgElementType === 'svg') props['data-xmlns'] = this.svgNS;

return {
svgElementType: svgElementType,
props: props,
children: [],
setAttribute: function(propertyName, value) {
if (propertyName === 'class') propertyName = 'className';

this.props[propertyName] = value;
},

get style() {
return props.style;
},
};
},

applyAttributes: function(element, attributes) {
for(var propertyName in attributes) {
var _propertyName = propertyName.replace(
/-([a-z])/g,
function (g) { return g[1].toUpperCase(); }
);

element.props[_propertyName] = attributes[propertyName];
}

return element;
},

fillText: function(text, x, y) {
var attributes = {};
Vex.Merge(attributes, this.attributes);

var path = this.create('path');
var fontSize = this.getFontSize();
var pathData = this.fontObj.getPath(text, x, y, fontSize).toPathData();

attributes.d = pathData;
attributes.stroke = "none";
attributes.x = x;
attributes.y = y;

this.applyAttributes(path, attributes);
this.add(path);
},

add: function(element) {
this.parent.children.push(element);
},

getFontSize: function() {
var fontSize = Number(this.attributes['font-size'].replace(/[^.\d]+/g, ''));

// Convert pt to px
if (/pt$/.test(this.attributes['font-size'])) {
fontSize = (fontSize * 4 / 3) | 0;
}

return fontSize;
},

measureText: function(text) {
var fontSize = this.getFontSize();
var pathData = this.fontObj.getPath(text, 0, 0, fontSize).toPathData();

return this.getBoundingBox(pathData);
},

createReactElement: function(element) {
var children = [];

for (var i = 0; i < element.children.length; i++) {
children.push(this.createReactElement(element.children[i]));
}

return this.React.createElement(
element.svgElementType, element.props, children
);
},

toSVG: function() {
var reactElement = this.createReactElement(this.svg);
var svgData = this.ReactDOMServer.renderToStaticMarkup(reactElement);

// React v0.15 does not support xmlns attribute, so used like this
return svgData.replace('data-xmlns', 'xmlns');
},

iePolyfill: function() {}
});

return TextSVGContext;
}());
49 changes: 25 additions & 24 deletions tests/vexflow_test_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

// Mock out the QUnit stuff for generating svg images,
// since we don't really care about the assertions.
if (typeof window === 'undefined') window = {};

if (!window.QUnit) {
window.QUnit = {}
QUnit = {};
window.QUnit = QUnit;

QUnit.assertions = {
ok: function() {return true;},
Expand Down Expand Up @@ -47,6 +50,9 @@ VF.Test = (function() {
// Where images are stored for NodeJS tests.
NODE_IMAGEDIR: "images",

// Init options for TextSVGContext
NodeOptions: {},

// Default font properties for tests.
Font: {size: 10},

Expand Down Expand Up @@ -155,33 +161,28 @@ VF.Test = (function() {
}

QUnit.test(name, function(assert) {
var div = document.createElement("div");
div.setAttribute("id", "canvas_" + VF.Test.genID());
document.getElementsByTagName('body')[0].appendChild(div);

func({
canvas_sel: div,
canvas_sel: VF.Test.NodeOptions,
params: params,
assert: assert },
VF.Renderer.getSVGContext);

if (VF.Renderer.lastContext != null) {
// If an SVG context was used, then serialize and save its contents to
// a local file.
var svgData = new XMLSerializer().serializeToString(VF.Renderer.lastContext.svg);

var moduleName = sanitizeName(QUnit.current_module);
var testName = sanitizeName(QUnit.current_test);
var filename = VF.Test.NODE_IMAGEDIR + "/" + moduleName + "." + testName + ".svg";
try {
fs.write(filename, svgData, "w");
} catch(e) {
console.log("Can't save file: " + filename + ". Error: " + e);
slimer.exit();
};
VF.Renderer.lastContext = null;
}
VF.Renderer.getTextSVGContext);
});

if (VF.Renderer.lastContext == null) {
console.log('VF.Renderer.lastContext does not exist!');
return;
}

var svgData = VF.Renderer.lastContext.toSVG();
var moduleName = sanitizeName(QUnit.current_module);
var testName = sanitizeName(QUnit.current_test);
var filename = VF.Test.NODE_IMAGEDIR + "/" + moduleName + "." + testName + ".svg";

try {
fs.writeFileSync(filename, svgData);
} catch(e) {
console.log("Can't save file: " + filename + ". Error: " + e);
}
},

plotNoteWidth: VF.Note.plotMetrics,
Expand Down
Binary file added tools/NotoSans-Regular.ttf
Binary file not shown.
28 changes: 20 additions & 8 deletions tools/generate_svg_images.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
$ for f in *.svg; do echo $f; rsvg-convert $f `basename $f .svg`.png; done
*/
var fs = require('fs');
var system = require('system');
var args = system.args;
var Vex = require(args[1] + '/vexflow-debug.js');
Vex.Flow.Test = require(args[1] + '/vexflow-tests.js');
var mkdirp = require('mkdirp');
var process = require('process');
var argv = process.argv;
var Vex = require(argv[2] + '/vexflow-debug.js');
Vex.Flow.Test = require(argv[2] + '/vexflow-tests.js');
var VF = Vex.Flow;

// Tell VexFlow that we're outside the browser -- just run
Expand All @@ -21,12 +22,23 @@ VF.Test.RUN_CANVAS_TESTS = false;
VF.Test.RUN_SVG_TESTS = false;
VF.Test.RUN_RAPHAEL_TESTS = false;
VF.Test.RUN_NODE_TESTS = true;
VF.Test.NODE_IMAGEDIR = args[2];
VF.Test.NODE_IMAGEDIR = argv[3];

// Create the image directory if it doesn't exist.
fs.makeTree(VF.Test.NODE_IMAGEDIR);
mkdirp.sync(VF.Test.NODE_IMAGEDIR);

var React = require('react');
var ReactDOMServer = require('react-dom/server');
var getBoundingBox = require('svg-path-bounding-box');
var opentype = require('opentype.js');
var font = opentype.loadSync('./tools/NotoSans-Regular.ttf');

VF.Test.NodeOptions = {
React: React,
ReactDOMServer: ReactDOMServer,
getBoundingBox: getBoundingBox,
font: font,
};

// Run all tests.
VF.Test.run();

slimer.exit();

0 comments on commit c6727ef

Please sign in to comment.