From af142b24ff8c2fdb559fe697b7c8a46047f43947 Mon Sep 17 00:00:00 2001 From: mkwiser Date: Sun, 5 Oct 2014 14:10:29 +0800 Subject: [PATCH] add svg 2 ttf object --- demo/js/svg2ttfobject.js | 49 ++++++++ demo/svg2ttfobject.html | 34 ++++++ src/math/bezierCubic2Q2.js | 33 ++++++ src/ttf/svg2ttfobject.js | 213 +++++++++++++++++++++++++++++++++++ src/ttf/table/glyfwriter.js | 7 +- src/ttf/table/table.js | 2 +- src/ttf/ttf2svg.js | 19 +--- src/ttf/util/contours2svg.js | 34 ++++++ src/ttf/util/svg2contours.js | 172 ++++++++++++++++++++++++++++ src/ttf/writer.js | 19 +++- 10 files changed, 556 insertions(+), 26 deletions(-) create mode 100644 demo/js/svg2ttfobject.js create mode 100644 demo/svg2ttfobject.html create mode 100644 src/math/bezierCubic2Q2.js create mode 100644 src/ttf/svg2ttfobject.js create mode 100644 src/ttf/util/contours2svg.js create mode 100644 src/ttf/util/svg2contours.js diff --git a/demo/js/svg2ttfobject.js b/demo/js/svg2ttfobject.js new file mode 100644 index 0000000..f1967b9 --- /dev/null +++ b/demo/js/svg2ttfobject.js @@ -0,0 +1,49 @@ +/** + * @file svg2ttfobject.js + * @author mengke01 + * @date + * @description + * svg转ttfobject + */ + +define( + function(require) { + + var svg2ttfobject = require('ttf/svg2ttfobject'); + var ttf2base64 = require('ttf/ttf2base64'); + var TTFWriter = require('ttf/ttfwriter'); + + var entry = { + + /** + * 初始化 + */ + init: function () { + + $.ajax({ + url: '../font/iconfont.svg', + dataType: 'text' + }).done(function(data) { + + var ttfObject = svg2ttfobject(data); + var writer = new TTFWriter(); + + var ttfBuffer = writer.write(ttfObject); + var base64str = ttf2base64(ttfBuffer); + + var saveBtn = $('.saveas'); + saveBtn.attr('href', base64str); + saveBtn.attr('download', 'save.ttf'); + + + console.log(ttfObject); + }); + + } + }; + + entry.init(); + + return entry; + } +); \ No newline at end of file diff --git a/demo/svg2ttfobject.html b/demo/svg2ttfobject.html new file mode 100644 index 0000000..2035ed0 --- /dev/null +++ b/demo/svg2ttfobject.html @@ -0,0 +1,34 @@ + + + + + svg2ttfobject test + + + + + + + +保存 + + + + + + + + + + + \ No newline at end of file diff --git a/src/math/bezierCubic2Q2.js b/src/math/bezierCubic2Q2.js new file mode 100644 index 0000000..3d6abb7 --- /dev/null +++ b/src/math/bezierCubic2Q2.js @@ -0,0 +1,33 @@ +/** + * @file bezierCubic2Q2.js + * @author mengke01 + * @date + * @description + * 三次贝塞尔转二次贝塞尔 + * + * references: + * https://github.com/search?utf8=%E2%9C%93&q=svg2ttf + * http://www.caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + * + */ + +define( + function(require) { + + /** + * 三次贝塞尔转二次贝塞尔 + * + * @param {Object} p1 开始点 + * @param {Object} c1 控制点1 + * @param {Object} c2 控制点2 + * @param {Object} p2 结束点 + * @return {Array} 二次贝塞尔控制点 + */ + function bezierCubic2Q2(p1, c1, c2, p2) { + + return []; + } + + return bezierCubic2Q2; + } +); diff --git a/src/ttf/svg2ttfobject.js b/src/ttf/svg2ttfobject.js new file mode 100644 index 0000000..fd4ad18 --- /dev/null +++ b/src/ttf/svg2ttfobject.js @@ -0,0 +1,213 @@ +/** + * @file svg2ttfObject.js + * @author mengke01 + * @date + * @description + * svg格式转ttfObject格式 + */ + + +define( + function(require) { + var string = require('common/string'); + var svg2contours = require('./util/svg2contours'); + var computeBoundingBox = require('graphics/computeBoundingBox'); + + /** + * 加载xml字符串 + * + * @param {string} xml xml字符串 + * @return {XMLDocument} + */ + function loadXML(xml) { + if (document.implementation && document.implementation.createDocument) { + var domParser = new DOMParser(); + xmlDoc = domParser.parseFromString(xml, 'text/xml'); + return xmlDoc; + } + throw 'not support xml parser'; + } + + /** + * 获取空的ttfObject + * + * @return {Object} ttfObject对象 + */ + function getEmptyObject() { + return { + 'OS/2': {}, + 'name': {}, + 'hhea': {}, + 'head': {}, + 'post': {}, + 'glyf': [] + }; + } + + + /** + * 对ttfObject进行处理,去除小数 + * + * @param {Object} ttf ttfObject + * @return {Object} ttfObject + */ + function resolve(ttf) { + + ttf.glyf.forEach(function(glyf) { + if (glyf.contours && glyf.contours.length) { + + glyf.contours.forEach(function(contour) { + contour.forEach(function(p) { + p.x = Math.round(p.x); + p.y = Math.round(p.y); + }); + }); + + var bound = computeBoundingBox.computePathBox.apply(null, glyf.contours); + glyf.xMin = bound.x; + glyf.xMax = bound.x + bound.width; + glyf.yMin = bound.y; + glyf.yMax = bound.y + bound.height; + glyf.advanceWidth = Math.round(glyf.advanceWidth); + } + }); + + ttf.head.xMin = Math.round(ttf.head.xMin); + ttf.head.xMax = Math.round(ttf.head.xMax); + ttf.head.yMin = Math.round(ttf.head.yMin); + ttf.head.yMax = Math.round(ttf.head.yMax); + ttf.head.unitsPerEm = Math.round(ttf.head.unitsPerEm || 1024); + + return ttf; + } + + + /** + * 解析xml文档 + * + * @param {XMLDocument} xmlDoc XML文档对象 + * @return {Object} 解析后对象 + */ + function parseXML(xmlDoc) { + + var ttf =getEmptyObject(); + var svgNode = xmlDoc.getElementsByTagName('svg')[0]; + + if (!svgNode) { + throw 'not a svg file format!'; + } + + var metaNode = xmlDoc.getElementsByTagName('metadata')[0]; + var fontNode = xmlDoc.getElementsByTagName('font')[0]; + var fontFaceNode = xmlDoc.getElementsByTagName('font-face')[0]; + var missingNode = xmlDoc.getElementsByTagName('missing-glyph')[0]; + + if (metaNode && metaNode.textContent) { + ttf.metadata = string.decodeHTML(metaNode.textContent.trim()); + } + + // 解析font + if (fontNode) { + ttf.id = fontNode.getAttribute('id') || ''; + ttf.hhea.advanceWidthMax = +(fontNode.getAttribute('horiz-adv-x') || 0); + } + + if (fontFaceNode) { + var OS2 = ttf['OS/2']; + ttf.name.fontFamily = fontFaceNode.getAttribute('font-family') || ''; + OS2.usWeightClass = +(fontFaceNode.getAttribute('font-weight') || 0); + ttf.head.unitsPerEm = +(fontFaceNode.getAttribute('units-per-em') || 0); + + // 解析panose, eg: 2 0 6 3 0 0 0 0 0 0 + var panose = (fontFaceNode.getAttribute('panose-1') || '').split(' '); + ['bFamilyType', 'bSerifStyle', 'bWeight', 'bProportion', 'bContrast', + 'bStrokeVariation', 'bArmStyle', 'bLetterform', 'bMidline', 'bXHeight'].forEach(function(name, i) { + OS2[name] = +(panose[i] || 0); + }); + + ttf.hhea.ascent = +(fontFaceNode.getAttribute('ascent') || 0); + ttf.hhea.descent = +(fontFaceNode.getAttribute('descent') || 0); + OS2.bXHeight = +(fontFaceNode.getAttribute('x-height') || 0); + + // 解析bounding + var box = (fontFaceNode.getAttribute('bbox') || '').split(' '); + ['xMin', 'yMin', 'xMax', 'yMax'].forEach(function(name, i) { + ttf.head[name] = +(box[i] || ''); + }); + + ttf.post.underlineThickness = +(fontFaceNode.getAttribute('underline-thickness') || 0); + ttf.post.underlinePosition = +(fontFaceNode.getAttribute('underline-position') || 0); + + // unicode range + var unicodeRange = fontFaceNode.getAttribute('unicode-range'); + if (unicodeRange) { + unicodeRange.replace(/u\+([0-9A-Z]+)(\-[0-9A-Z]+)?/i, function ($0, a, b) { + OS2.usFirstCharIndex = Number('0x' + a); + OS2.usLastCharIndex = b ? Number('0x' + b.slice(1)) : 0xFFFFFFFF; + }); + } + } + + // 解析glyf + var d, unicode; + if (missingNode) { + var missing = { + advanceWidth: +(missingNode.getAttribute('horiz-adv-x') || 0) + }; + + if (d = missingNode.getAttribute('d')) { + missing.contours = svg2contours(d); + } + } + + var glyfNodes = xmlDoc.getElementsByTagName('glyph'); + + // path 对象也支持 + if (!glyfNodes.length) { + glyfNodes = xmlDoc.getElementsByTagName('path'); + } + + if (glyfNodes.length) { + for (var i = 0, l = glyfNodes.length; i < l ; i++) { + + var node = glyfNodes[i]; + var glyf = { + advanceWidth: +(node.getAttribute('horiz-adv-x') || 0), + name: node.getAttribute('glyph-name') || node.getAttribute('name') || '' + }; + + if (unicode = node.getAttribute('unicode')) { + glyf.unicode = unicode.split('').map(function(u) { + return u.charCodeAt(0); + }); + } + + if (d = node.getAttribute('d')) { + glyf.contours = svg2contours(d); + } + ttf.glyf.push(glyf); + } + } + + return ttf; + } + + /** + * svg格式转ttfObject格式 + * + * @param {string} svg svg格式 + * @return {Object} ttfObject + */ + function svg2ttfObject(svg) { + + var xmlDoc = svg; + if (typeof(svg) === 'string') { + xmlDoc = loadXML(svg); + } + var ttf = parseXML(xmlDoc); + return resolve(ttf); + } + + return svg2ttfObject; + } +); diff --git a/src/ttf/table/glyfwriter.js b/src/ttf/table/glyfwriter.js index 93854e0..2030db8 100644 --- a/src/ttf/table/glyfwriter.js +++ b/src/ttf/table/glyfwriter.js @@ -21,7 +21,7 @@ define( */ function sizeof(glyf, glyfSupport) { - if (!glyf.contours.length) { + if (!glyf.contours || 0 === glyf.contours.length) { return 0; } @@ -96,7 +96,7 @@ define( */ function getFlags(glyf, glyfSupport) { - if (!glyf.contours.length) { + if (!glyf.contours || 0 === glyf.contours.length) { return glyfSupport; } @@ -231,7 +231,8 @@ define( var writerOffset = writer.offset; - if (!glyf.compound && 0 == glyf.contours.length) { + // 非复合图元没有轮廓则不写 + if (!glyf.compound && (!glyf.contours || 0 == glyf.contours.length)) { return; } diff --git a/src/ttf/table/table.js b/src/ttf/table/table.js index 2753ab5..3c07d1b 100644 --- a/src/ttf/table/table.js +++ b/src/ttf/table/table.js @@ -119,7 +119,7 @@ define( break; case struct.String: - writer.writeString(table[name]); + writer.writeString(table[name], item[2] || 0); break; default: diff --git a/src/ttf/ttf2svg.js b/src/ttf/ttf2svg.js index eed29c8..bcc6d7b 100644 --- a/src/ttf/ttf2svg.js +++ b/src/ttf/ttf2svg.js @@ -14,7 +14,7 @@ define( var string = require('common/string'); var TTFReader = require('./ttfreader'); - var contour2svg = require('./util/contour2svg'); + var contours2svg = require('./util/contours2svg'); // svg font id var SVG_FONT_ID = 'fonteditor'; @@ -38,19 +38,6 @@ define( // glyph 模板 var GLYPH_TPL = ''; - - /** - * glyf轮廓转svgpath - * - * @param {Array} contours 轮廓list - * @return {string} path字符串 - */ - function glyf2svg(contours) { - return contours.map(function(contour) { - return contour2svg(contour); - }).join(''); - } - /** * unicode 转xml编码格式 * @@ -104,7 +91,7 @@ define( xmlObject.missing = {}; xmlObject.missing.advanceWidth = ttf.glyf[0].advanceWidth || 0; xmlObject.missing.d = ttf.glyf[0].contours && ttf.glyf[0].contours.length - ? glyf2svg(ttf.glyf[0].contours) : ''; + ? contours2svg(ttf.glyf[0].contours) : ''; // glyf 信息 var glyphList = ''; @@ -116,7 +103,7 @@ define( var glyfObject = { name: glyf.name, unicode: unicode2xml(glyf.unicode), - d: glyf2svg(glyf.contours) + d: contours2svg(glyf.contours) } glyphList += string.format(GLYPH_TPL, glyfObject); } diff --git a/src/ttf/util/contours2svg.js b/src/ttf/util/contours2svg.js new file mode 100644 index 0000000..c68f480 --- /dev/null +++ b/src/ttf/util/contours2svg.js @@ -0,0 +1,34 @@ +/** + * @file contours2svg.js + * @author mengke01 + * @date + * @description + * contours 转 svg path + */ + + +define( + function(require) { + + var contour2svg = require('./contour2svg'); + + /** + * contours轮廓转svgpath + * + * @param {Array} contours 轮廓list + * @return {string} path字符串 + */ + function contours2svg(contours) { + + if (!contours.length) { + return ''; + } + + return contours.map(function(contour) { + return contour2svg(contour); + }).join(''); + } + + return contours2svg; + } +); diff --git a/src/ttf/util/svg2contours.js b/src/ttf/util/svg2contours.js new file mode 100644 index 0000000..0869c1b --- /dev/null +++ b/src/ttf/util/svg2contours.js @@ -0,0 +1,172 @@ +/** + * @file svg2contours.js + * @author mengke01 + * @date + * @description + * svg d 转换为contours + */ + + +define( + function(require) { + + /** + * svg path 转 contours + * + * @param {string} path path对象 + * @return {Array} contours + */ + function svg2contours(path) { + + if (!path || !path.length) { + return null; + } + + // 获取segments + var segments = []; + var cmd, lastIndex, args, segReg = /\-?\d+(?:\.\d+)?\b/g; + for (var i = 0, l = path.length;i < l; i++) { + var c = path.charAt(i).toUpperCase(); + switch (c) { + case 'M': + if (i == 0) { + cmd = c; + lastIndex = 1; + break; + } + case 'Q': + case 'T': + case 'C': + case 'S': + case 'H': + case 'V': + case 'L': + case 'A': + case 'Z': + if (cmd == 'Z') { + segments.push({cmd:'Z'}); + } + else { + args = path.slice(lastIndex, i); + segments.push({ + cmd: cmd, + args: args.match(segReg).map(function(d) { + return +d.trim(); + }) + }); + } + + cmd = c; + lastIndex = i + 1; + break; + } + } + + segments.push({cmd:'Z'}); + + // 解析segments + var contours = [], contour = [], prevX = 0, prevY = 0; + for (var i = 0, l = segments.length;i < l; i++) { + segment = segments[i]; + + if (segment.cmd === 'Z') { + contours.push(contour); + contour = []; + } + else if (segment.cmd === 'M') { + prevX = segment.args[0]; + prevY = segment.args[1]; + contour.push({ + x: prevX, + y: prevY, + onCurve: true + }); + } + else if (segment.cmd === 'H') { + prevX += segment.args[0]; + contour.push({ + x: prevX, + y: prevY, + onCurve: true + }); + } + else if (segment.cmd === 'V') { + prevY += segment.args[0]; + contour.push({ + x: prevX, + y: prevY, + onCurve: true + }); + } + else if (segment.cmd === 'L') { + prevX += segment.args[0]; + prevY += segment.args[1]; + contour.push({ + x: prevX, + y: prevY, + onCurve: true + }); + } + // 二次贝塞尔 + else if (segment.cmd === 'Q') { + // 这里可能会连续绘制,最后一个是终点 + var q = 0, ql = segment.args.length - 2; + for (; q < ql ; q += 2) { + contour.push({ + x: prevX + segment.args[q], + y: prevY + segment.args[q + 1] + }); + } + + prevX += segment.args[ql]; + prevY += segment.args[ql + 1]; + + contour.push({ + x: prevX, + y: prevY, + onCurve: true + }); + } + // 二次贝塞尔平滑 + else if (segment.cmd === 'T') { + + var pc = contour[contour.length - 2]; + + // 这里需要移除上一个曲线的终点 + contour.pop(); + + contour.push({ + x: 2 * prevX - pc.x, + y: 2 * prevY - pc.y + }); + + prevX += segment.args[0]; + prevY += segment.args[1]; + contour.push({ + x: prevX, + y: prevY, + onCurve: true + }); + } + // 三次贝塞尔 + else if (segment.cmd === 'C') { + throw 'not support cubic bezier'; + } + // 三次贝塞尔平滑 + else if (segment.cmd === 'S') { + throw 'not support cubic bezier'; + } + // 圆弧 + else if (segment.cmd === 'A') { + throw 'not support arc'; + } + } + + return contours; + } + + + + return svg2contours; + } +); diff --git a/src/ttf/writer.js b/src/ttf/writer.js index 701fa2b..dca1e68 100644 --- a/src/ttf/writer.js +++ b/src/ttf/writer.js @@ -107,12 +107,13 @@ define( * @param {number} length 长度 * @return {this} */ - writeString: function(str, offset) { - + writeString: function(str, length, offset) { + str = str || ''; if(undefined === offset) { offset = this.offset; } - var length = str.replace(/[^\x00-\xff]/g, '11').length; + var length = length || str.replace(/[^\x00-\xff]/g, '11').length; + if(length < 0 || offset + length > this.length) { throw 'length out of range:' + offset + ',' + length; } @@ -120,7 +121,7 @@ define( this.seek(offset); for (var i = 0, l = str.length, charCode; i < l; ++i) { - charCode = str.charCodeAt(i); + charCode = str.charCodeAt(i) || 0; if (charCode > 127) { // unicode编码可能会超出2字节, 写入与编码有关系,此处不做处理 // FIXME @@ -156,6 +157,7 @@ define( // ArrayBuffer this.seek(offset); + if (value instanceof ArrayBuffer) { var view = new DataView(value, 0, length); var littleEndian = this.littleEndian; @@ -256,13 +258,18 @@ define( offset = this.offset; } - if (typeof value.getTime === 'function') { + var delta = -2077545600000; // new Date(1970, 1, 1).getTime() - new Date(1904, 1, 1).getTime(); + + if (typeof value === 'undefined') { + value = delta; + } + else if (typeof value.getTime === 'function') { value = value.getTime(); } else { value = Date.parse(value); } - var delta = -2077545600000; // new Date(1970, 1, 1).getTime() - new Date(1904, 1, 1).getTime(); + var time = Math.round((value - delta) / 1000); this.writeUint32(0, offset); this.writeUint32(time, offset + 4);