UNPKG

opentype.js

Version:
362 lines (313 loc) 11.8 kB
// The Glyph object import check from './check'; import draw from './draw'; import Path from './path'; // import glyf from './tables/glyf' Can't be imported here, because it's a circular dependency function getPathDefinition(glyph, path) { let _path = path || new Path(); return { configurable: true, get: function() { if (typeof _path === 'function') { _path = _path(); } return _path; }, set: function(p) { _path = p; } }; } /** * @typedef GlyphOptions * @type Object * @property {string} [name] - The glyph name * @property {number} [unicode] * @property {Array} [unicodes] * @property {number} [xMin] * @property {number} [yMin] * @property {number} [xMax] * @property {number} [yMax] * @property {number} [advanceWidth] */ // A Glyph is an individual mark that often corresponds to a character. // Some glyphs, such as ligatures, are a combination of many characters. // Glyphs are the basic building blocks of a font. // // The `Glyph` class contains utility methods for drawing the path and its points. /** * @exports opentype.Glyph * @class * @param {GlyphOptions} * @constructor */ function Glyph(options) { // By putting all the code on a prototype function (which is only declared once) // we reduce the memory requirements for larger fonts by some 2% this.bindConstructorValues(options); } /** * @param {GlyphOptions} */ Glyph.prototype.bindConstructorValues = function(options) { this.index = options.index || 0; // These three values cannot be deferred for memory optimization: this.name = options.name || null; this.unicode = options.unicode || undefined; this.unicodes = options.unicodes || options.unicode !== undefined ? [options.unicode] : []; // But by binding these values only when necessary, we reduce can // the memory requirements by almost 3% for larger fonts. if ('xMin' in options) { this.xMin = options.xMin; } if ('yMin' in options) { this.yMin = options.yMin; } if ('xMax' in options) { this.xMax = options.xMax; } if ('yMax' in options) { this.yMax = options.yMax; } if ('advanceWidth' in options) { this.advanceWidth = options.advanceWidth; } // The path for a glyph is the most memory intensive, and is bound as a value // with a getter/setter to ensure we actually do path parsing only once the // path is actually needed by anything. Object.defineProperty(this, 'path', getPathDefinition(this, options.path)); }; /** * @param {number} */ Glyph.prototype.addUnicode = function(unicode) { if (this.unicodes.length === 0) { this.unicode = unicode; } this.unicodes.push(unicode); }; /** * Calculate the minimum bounding box for this glyph. * @return {opentype.BoundingBox} */ Glyph.prototype.getBoundingBox = function() { return this.path.getBoundingBox(); }; /** * Convert the glyph to a Path we can draw on a drawing context. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. * @param {Object=} options - xScale, yScale to stretch the glyph. * @param {opentype.Font} if hinting is to be used, the font * @return {opentype.Path} */ Glyph.prototype.getPath = function(x, y, fontSize, options, font) { x = x !== undefined ? x : 0; y = y !== undefined ? y : 0; fontSize = fontSize !== undefined ? fontSize : 72; let commands; let hPoints; if (!options) options = { }; let xScale = options.xScale; let yScale = options.yScale; if (options.hinting && font && font.hinting) { // in case of hinting, the hinting engine takes care // of scaling the points (not the path) before hinting. hPoints = this.path && font.hinting.exec(this, fontSize); // in case the hinting engine failed hPoints is undefined // and thus reverts to plain rending } if (hPoints) { // Call font.hinting.getCommands instead of `glyf.getPath(hPoints).commands` to avoid a circular dependency commands = font.hinting.getCommands(hPoints); x = Math.round(x); y = Math.round(y); // TODO in case of hinting xyScaling is not yet supported xScale = yScale = 1; } else { commands = this.path.commands; const scale = 1 / (this.path.unitsPerEm || 1000) * fontSize; if (xScale === undefined) xScale = scale; if (yScale === undefined) yScale = scale; } const p = new Path(); for (let i = 0; i < commands.length; i += 1) { const cmd = commands[i]; if (cmd.type === 'M') { p.moveTo(x + (cmd.x * xScale), y + (-cmd.y * yScale)); } else if (cmd.type === 'L') { p.lineTo(x + (cmd.x * xScale), y + (-cmd.y * yScale)); } else if (cmd.type === 'Q') { p.quadraticCurveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale), x + (cmd.x * xScale), y + (-cmd.y * yScale)); } else if (cmd.type === 'C') { p.curveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale), x + (cmd.x2 * xScale), y + (-cmd.y2 * yScale), x + (cmd.x * xScale), y + (-cmd.y * yScale)); } else if (cmd.type === 'Z') { p.closePath(); } } return p; }; /** * Split the glyph into contours. * This function is here for backwards compatibility, and to * provide raw access to the TrueType glyph outlines. * @return {Array} */ Glyph.prototype.getContours = function() { if (this.points === undefined) { return []; } const contours = []; let currentContour = []; for (let i = 0; i < this.points.length; i += 1) { const pt = this.points[i]; currentContour.push(pt); if (pt.lastPointOfContour) { contours.push(currentContour); currentContour = []; } } check.argument(currentContour.length === 0, 'There are still points left in the current contour.'); return contours; }; /** * Calculate the xMin/yMin/xMax/yMax/lsb/rsb for a Glyph. * @return {Object} */ Glyph.prototype.getMetrics = function() { const commands = this.path.commands; const xCoords = []; const yCoords = []; for (let i = 0; i < commands.length; i += 1) { const cmd = commands[i]; if (cmd.type !== 'Z') { xCoords.push(cmd.x); yCoords.push(cmd.y); } if (cmd.type === 'Q' || cmd.type === 'C') { xCoords.push(cmd.x1); yCoords.push(cmd.y1); } if (cmd.type === 'C') { xCoords.push(cmd.x2); yCoords.push(cmd.y2); } } const metrics = { xMin: Math.min.apply(null, xCoords), yMin: Math.min.apply(null, yCoords), xMax: Math.max.apply(null, xCoords), yMax: Math.max.apply(null, yCoords), leftSideBearing: this.leftSideBearing }; if (!isFinite(metrics.xMin)) { metrics.xMin = 0; } if (!isFinite(metrics.xMax)) { metrics.xMax = this.advanceWidth; } if (!isFinite(metrics.yMin)) { metrics.yMin = 0; } if (!isFinite(metrics.yMax)) { metrics.yMax = 0; } metrics.rightSideBearing = this.advanceWidth - metrics.leftSideBearing - (metrics.xMax - metrics.xMin); return metrics; }; /** * Draw the glyph on the given context. * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. * @param {Object=} options - xScale, yScale to stretch the glyph. */ Glyph.prototype.draw = function(ctx, x, y, fontSize, options) { this.getPath(x, y, fontSize, options).draw(ctx); }; /** * Draw the points of the glyph. * On-curve points will be drawn in blue, off-curve points will be drawn in red. * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. */ Glyph.prototype.drawPoints = function(ctx, x, y, fontSize) { function drawCircles(l, x, y, scale) { ctx.beginPath(); for (let j = 0; j < l.length; j += 1) { ctx.moveTo(x + (l[j].x * scale), y + (l[j].y * scale)); ctx.arc(x + (l[j].x * scale), y + (l[j].y * scale), 2, 0, Math.PI * 2, false); } ctx.closePath(); ctx.fill(); } x = x !== undefined ? x : 0; y = y !== undefined ? y : 0; fontSize = fontSize !== undefined ? fontSize : 24; const scale = 1 / this.path.unitsPerEm * fontSize; const blueCircles = []; const redCircles = []; const path = this.path; for (let i = 0; i < path.commands.length; i += 1) { const cmd = path.commands[i]; if (cmd.x !== undefined) { blueCircles.push({x: cmd.x, y: -cmd.y}); } if (cmd.x1 !== undefined) { redCircles.push({x: cmd.x1, y: -cmd.y1}); } if (cmd.x2 !== undefined) { redCircles.push({x: cmd.x2, y: -cmd.y2}); } } ctx.fillStyle = 'blue'; drawCircles(blueCircles, x, y, scale); ctx.fillStyle = 'red'; drawCircles(redCircles, x, y, scale); }; /** * Draw lines indicating important font measurements. * Black lines indicate the origin of the coordinate system (point 0,0). * Blue lines indicate the glyph bounding box. * Green line indicates the advance width of the glyph. * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. */ Glyph.prototype.drawMetrics = function(ctx, x, y, fontSize) { let scale; x = x !== undefined ? x : 0; y = y !== undefined ? y : 0; fontSize = fontSize !== undefined ? fontSize : 24; scale = 1 / this.path.unitsPerEm * fontSize; ctx.lineWidth = 1; // Draw the origin ctx.strokeStyle = 'black'; draw.line(ctx, x, -10000, x, 10000); draw.line(ctx, -10000, y, 10000, y); // This code is here due to memory optimization: by not using // defaults in the constructor, we save a notable amount of memory. const xMin = this.xMin || 0; let yMin = this.yMin || 0; const xMax = this.xMax || 0; let yMax = this.yMax || 0; const advanceWidth = this.advanceWidth || 0; // Draw the glyph box ctx.strokeStyle = 'blue'; draw.line(ctx, x + (xMin * scale), -10000, x + (xMin * scale), 10000); draw.line(ctx, x + (xMax * scale), -10000, x + (xMax * scale), 10000); draw.line(ctx, -10000, y + (-yMin * scale), 10000, y + (-yMin * scale)); draw.line(ctx, -10000, y + (-yMax * scale), 10000, y + (-yMax * scale)); // Draw the advance width ctx.strokeStyle = 'green'; draw.line(ctx, x + (advanceWidth * scale), -10000, x + (advanceWidth * scale), 10000); }; export default Glyph;