├── .gitignore ├── .npmignore ├── License.md ├── README.md ├── index.js ├── lib ├── engines │ └── node.js ├── util │ └── util.js └── webfont.js ├── package.json └── test ├── dest ├── icons.eot ├── icons.svg ├── icons.ttf ├── icons.woff └── icons.woff2 └── src ├── i-right.svg ├── i-wrong.svg ├── mailru.svg ├── odnoklassniki.svg ├── pinterest.svg ├── plusone.svg └── single.svg /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/tmp 3 | .cache 4 | ._* 5 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Formula 3 | test -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright © 2014 Artem Sapegin, http://sapegin.me 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## fis-command-webfont 2 | 3 | --- 4 | 5 | 6 | ## 背景与简介 7 | 8 | 目前移动端webfont字体使用越来越广泛,由于缺少比较好的自动化工具,开发者在修改字体图标时需要在2个平台进行转换(svg转ttf再转woff2,woff2普遍不支持)才能完成字体生成工作。 9 | 10 | 经过调研,基于[grunt-webfont](https://github.com/sapegin/grunt-webfont)改造成fis插件,实现一键转换svg图标为svg,oet,ttf,woff,woff2的功能。 11 | 12 | 由于开源模块大量使用了异步调用,所以暂未改造成fis release 插件,先用command命令来实现自动化需求。 13 | 14 | 您可以通过第三方平台来了解字体生成与转换过程: https://icomoon.io/app/#/select/font 、 https://everythingfonts.com/# 15 | 16 | ## 开始使用 17 | 18 | ### 安装插件 19 | 20 | 执行 `npm install -g fis-command-webfont` 全局安装 21 | 22 | ### 配置插件 23 | 24 | 在fis-conf.js里面添加配置: 25 | 26 | 27 | ```javascript 28 | 29 | fis.config.set("webfont",{ 30 | 'src' : '/static/fonts/icons',//图标目录 31 | 'dest' : '/static/fonts', //产出字体目录 32 | 'fontname' : 'zuoye_font', //产出字体名称 33 | 'order' : 'name' //name或者time //图标按名称还是按修改时间排序,默认按名称排序 34 | }); 35 | 36 | ``` 37 | 38 | `注意`:每个图标都会递增生成对应的uinicode,为了避免更新后编码变动,建议icon按字母顺序加前缀并按名称排序 39 | 40 | 您也可以通过命令行方式传递参数,具体见`fis webfont -h`。 41 | 42 | ### 生成字体 43 | 44 | 在fis模块根目录执行`fis webfont`即可完成字体转换工作,字体将生成更新到dest目录里(原有同名字体将删除)。 45 | 46 | 47 | ## WOFF2问题 48 | 49 | WOFF2作为新一代字体标准比woff有明显的优势(大小减小30%),但浏览器支持度低,具体见[caniuse](http://caniuse.com/#search=woff2) 50 | 51 | 目前woff2的字体转换工具很少,没有相应的npm模块。Google提供的[方案](https://github.com/google/woff2)也在开发中且不支持windows,如果您想生成woff2字体,需要在本机(mac或linux)安装Google的转换工具: 52 | 53 | ``` 54 | git clone https://github.com/google/woff2.git 55 | cd woff2 56 | git submodule init 57 | git submodule update 58 | make clean all 59 | ``` 60 | 61 | 注意:编译完之后会在目录内产生`woff2_compress`和`woff2_decompress`,请将文件copy到PATH目录(mac上为`/usr/local/bin`)或者将当前目录添加到PATH中。 62 | 63 | 如果您不需要产出woff2字体,请在fis-conf.js配置里面添加一项指定产出字体: 64 | 65 | ``` 66 | types : 'svg,eot,woff,ttf' 67 | 68 | ``` 69 | 70 | 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * fis 3 | * http://fis.baidu.com/ 4 | */ 5 | 6 | 'use strict'; 7 | 8 | exports.name = 'webfont'; 9 | exports.usage = '[options]'; 10 | exports.desc = 'fis webfont generator,support svg,eot,ttf,woff,woff2'; 11 | 12 | var path = require('path'); 13 | var fs = require('fs'); 14 | var exec = require("child_process").exec; 15 | var exists = fs.existsSync; 16 | var webfont = require("./lib/webfont"); 17 | 18 | 19 | 20 | exports.register = function(commander) { 21 | 22 | commander 23 | .option('-n,--fontname ', 'set fontname ,default `iconfont`') 24 | .option('-s,--src ', 'set svg icon dir') 25 | .option('-d,--dest ', 'set font dir') 26 | .option('-r,--root ', 'set project root') 27 | .action(function() { 28 | var args = [].slice.call(arguments); 29 | var options = args.pop(); 30 | var root = path.join(process.cwd(), options.root || "" ); 31 | var filepath = path.resolve(root, 'fis-conf.js'); 32 | 33 | if(exists(filepath)){ 34 | require(filepath); 35 | } 36 | //读取配置,命令行参数优先 37 | var settings = fis.config.get("webfont") || {}; 38 | ['src','dest','fontname'].forEach(function(i){ 39 | settings[i] = options[i] || settings[i]; 40 | if(i == 'src' || i == 'dest'){ 41 | settings[i] = path.join(root,settings[i]); 42 | } 43 | }) 44 | 45 | 46 | if(!settings['src'] || !settings['dest'] ){ 47 | fis.log.error("please set webfont settings in fis-conf.js"); 48 | } 49 | 50 | //导出字体 51 | webfont.generateFonts(settings); 52 | 53 | }); 54 | }; 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/engines/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-webfont: Node.js engine 3 | * 4 | * @requires ttfautohint 1.00+ (optional) 5 | * @author Artem Sapegin (http://sapegin.me) 6 | */ 7 | 8 | module.exports = function(o, allDone) { 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var async = require('async'); 14 | var temp = require('temp'); 15 | var exec = require('exec'); 16 | var _ = require('lodash'); 17 | var StringDecoder = require('string_decoder').StringDecoder; 18 | var svgicons2svgfont = require('svgicons2svgfont'); 19 | var svg2ttf = require('svg2ttf'); 20 | var ttf2woff = require('ttf2woff'); 21 | var ttf2eot = require('ttf2eot'); 22 | var SVGO = require('svgo'); 23 | var MemoryStream = require('memorystream'); 24 | var logger = o.logger || require('winston'); 25 | var wf = require('../util/util'); 26 | 27 | // @todo Ligatures 28 | 29 | var fonts = {}; 30 | 31 | var generators = { 32 | svg: function(done) { 33 | var font = ''; 34 | var decoder = new StringDecoder('utf8'); 35 | svgFilesToStreams(o.files, function(streams) { 36 | var stream = svgicons2svgfont(streams, { 37 | fontName: o.fontName, 38 | fontHeight: o.fontHeight, 39 | descent: o.descent, 40 | normalize: o.normalize, 41 | round: o.round, 42 | log: logger.verbose.bind(logger), 43 | error: logger.error.bind(logger) 44 | }); 45 | stream.on('data', function(chunk) { 46 | font += decoder.write(chunk); 47 | }); 48 | stream.on('end', function() { 49 | fonts.svg = font; 50 | done(font); 51 | }); 52 | }); 53 | }, 54 | 55 | ttf: function(done) { 56 | getFont('svg', function(svgFont) { 57 | var font = svg2ttf(svgFont, {}); 58 | font = new Buffer(font.buffer); 59 | autohintTtfFont(font, function(hintedFont) { 60 | // ttfautohint is optional 61 | if (hintedFont) { 62 | font = hintedFont; 63 | } 64 | fonts.ttf = font; 65 | done(font); 66 | }); 67 | }); 68 | }, 69 | 70 | woff: function(done) { 71 | getFont('ttf', function(ttfFont) { 72 | var font = ttf2woff(new Uint8Array(ttfFont), {}); 73 | font = new Buffer(font.buffer); 74 | fonts.woff = font; 75 | done(font); 76 | }); 77 | }, 78 | 79 | woff2: function(done) { 80 | // Will be converted from TTF later 81 | done(); 82 | }, 83 | 84 | eot: function(done) { 85 | getFont('ttf', function(ttfFont) { 86 | var font = ttf2eot(new Uint8Array(ttfFont)); 87 | font = new Buffer(font.buffer); 88 | fonts.eot = font; 89 | done(font); 90 | }); 91 | } 92 | }; 93 | 94 | var steps = []; 95 | 96 | // Font types 97 | var typesToGenerate = o.types.slice(); 98 | if (o.types.indexOf('woff2') !== -1 && o.types.indexOf('ttf' === -1)) typesToGenerate.push('ttf'); 99 | typesToGenerate.forEach(function(type) { 100 | steps.push(createFontWriter(type)); 101 | }); 102 | 103 | steps.push(allDone); 104 | 105 | // Run! 106 | async.waterfall(steps); 107 | 108 | function getFont(type, done) { 109 | if (fonts[type]) { 110 | done(fonts[type]); 111 | } 112 | else { 113 | generators[type](done); 114 | } 115 | } 116 | 117 | function createFontWriter(type) { 118 | return function(done) { 119 | getFont(type, function(font) { 120 | fs.writeFileSync(wf.getFontPath(o, type), font); 121 | done(); 122 | }); 123 | }; 124 | } 125 | 126 | function svgFilesToStreams(files, done) { 127 | async.map(files, function(file, fileDone) { 128 | var svg = fs.readFileSync(file, 'utf8'); 129 | var svgo = new SVGO(); 130 | try { 131 | svgo.optimize(svg, function(res) { 132 | var idx = files.indexOf(file); 133 | var name = o.glyphs[idx]; 134 | var stream = new MemoryStream(res.data, { 135 | writable: false 136 | }); 137 | fileDone(null, { 138 | codepoint: o.codepoints[name], 139 | name: name, 140 | stream: stream 141 | }); 142 | }); 143 | } 144 | catch(err) { 145 | fileDone(err); 146 | } 147 | }, function(err, streams) { 148 | if (err) { 149 | logger.error('Can’t simplify SVG file with SVGO.\n\n' + err); 150 | allDone(false); 151 | } 152 | else { 153 | done(streams); 154 | } 155 | }); 156 | } 157 | 158 | function autohintTtfFont(font, done) { 159 | var tempDir = temp.mkdirSync(); 160 | var originalFilepath = path.join(tempDir, 'font.ttf'); 161 | var hintedFilepath = path.join(tempDir, 'hinted.ttf'); 162 | 163 | if (!o.autoHint){ 164 | done(false); 165 | return; 166 | } 167 | // Save original font to temporary directory 168 | fs.writeFileSync(originalFilepath, font); 169 | 170 | // Run ttfautohint 171 | var args = [ 172 | 'ttfautohint', 173 | '--symbol', 174 | '--fallback-script=latn', 175 | '--windows-compatibility', 176 | '--no-info', 177 | originalFilepath, 178 | hintedFilepath 179 | ]; 180 | 181 | exec(args, function(err, out, code) { 182 | if (err) { 183 | if (err instanceof Error) { 184 | if (err.code === 'ENOENT') { 185 | logger.verbose('Hinting skipped, ttfautohint not found.'); 186 | done(false); 187 | return; 188 | } 189 | err = err.message; 190 | } 191 | logger.error('Can’t run ttfautohint.\n\n' + err); 192 | done(false); 193 | return; 194 | } 195 | 196 | // Read hinted font back 197 | var hintedFont = fs.readFileSync(hintedFilepath); 198 | done(hintedFont); 199 | }); 200 | } 201 | 202 | }; 203 | -------------------------------------------------------------------------------- /lib/util/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-webfont: common stuff 3 | * 4 | * @author Artem Sapegin (http://sapegin.me) 5 | */ 6 | 7 | var path = require('path'); 8 | var glob = require('glob'); 9 | 10 | var exports = {}; 11 | 12 | /** 13 | * Unicode Private Use Area start. 14 | * http://en.wikipedia.org/wiki/Private_Use_(Unicode) 15 | * @type {Number} 16 | */ 17 | exports.UNICODE_PUA_START = 0xF101; 18 | 19 | /** 20 | * @font-face’s src values generation rules. 21 | * @type {Object} 22 | */ 23 | exports.fontsSrcsMap = { 24 | eot: [ 25 | { 26 | ext: '.eot' 27 | }, 28 | { 29 | ext: '.eot?#iefix', 30 | format: 'embedded-opentype' 31 | } 32 | ], 33 | woff: [ 34 | false, 35 | { 36 | ext: '.woff', 37 | format: 'woff', 38 | embeddable: true 39 | }, 40 | ], 41 | woff2: [ 42 | false, 43 | { 44 | ext: '.woff2', 45 | format: 'woff2', 46 | embeddable: true 47 | }, 48 | ], 49 | ttf: [ 50 | false, 51 | { 52 | ext: '.ttf', 53 | format: 'truetype', 54 | embeddable: true 55 | }, 56 | ], 57 | svg: [ 58 | false, 59 | { 60 | ext: '.svg#{fontBaseName}', 61 | format: 'svg' 62 | }, 63 | ] 64 | }; 65 | 66 | /** 67 | * CSS fileaname prefixes: _icons.scss. 68 | * @type {Object} 69 | */ 70 | exports.cssFilePrefixes = { 71 | _default: '', 72 | sass: '_', 73 | scss: '_' 74 | }; 75 | 76 | /** 77 | * @font-face’s src parts seperators. 78 | * @type {Object} 79 | */ 80 | exports.fontSrcSeparators = { 81 | _default: ',\n\t\t', 82 | styl: ', ' 83 | }; 84 | 85 | /** 86 | * List of available font formats. 87 | * @type {String} 88 | */ 89 | exports.fontFormats = 'eot,woff2,woff,ttf,svg'; 90 | 91 | /** 92 | * Returns list of all generated font files. 93 | * 94 | * @param {Object} o Options. 95 | * @return {Array} 96 | */ 97 | exports.generatedFontFiles = function(o) { 98 | var mask = '*.{' + o.types + '}'; 99 | return glob.sync(path.join(o.dest, o.fontBaseName + mask)); 100 | }; 101 | 102 | /** 103 | * Returns path to font of specified format. 104 | * 105 | * @param {Object} o Options. 106 | * @param {String} type Font type (see `wf.fontFormats`). 107 | * @return {String} 108 | */ 109 | exports.getFontPath = function(o, type) { 110 | return path.join(o.dest, o.fontName + '.' + type); 111 | }; 112 | 113 | // Expose 114 | module.exports = exports; 115 | -------------------------------------------------------------------------------- /lib/webfont.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SVG to webfont converter for Grunt 3 | * 4 | */ 5 | 6 | 7 | 'use strict'; 8 | 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var exec = require('exec'); 12 | var async = require('async'); 13 | var glob = require('glob'); 14 | var chalk = require('chalk'); 15 | var mkdirp = require('mkdirp'); 16 | var crypto = require('crypto'); 17 | var _ = require('lodash'); 18 | var _s = require('underscore.string'); 19 | var wf = require('./util/util'); 20 | 21 | 22 | 23 | module.exports.generateFonts = function(options){ 24 | 25 | var allDone = function(){ 26 | console.log("all done"); 27 | } 28 | 29 | //默认抹平icon的高度差异 30 | if(!options.hasOwnProperty('normalize')){ 31 | options['normalize'] = true; 32 | } 33 | if(!options.hasOwnProperty('autoHint')){ 34 | options['autoHint'] = false; 35 | } 36 | 37 | var md5 = crypto.createHash('md5'); 38 | 39 | /** 40 | * Winston to Grunt logger adapter. 41 | */ 42 | var logger = { 43 | warn: function() { 44 | console.log(arguments); 45 | }, 46 | error: function() { 47 | console.log(arguments); 48 | }, 49 | log: function() { 50 | console.log(arguments); 51 | }, 52 | verbose: function() { 53 | console.log(arguments); 54 | } 55 | }; 56 | 57 | 58 | // Source files 59 | var files = _.filter(fs.readdirSync(options.src), isSvgFile).map(function(file){ 60 | return path.join(options.src,file); 61 | }); 62 | 63 | var order = options['order'] || "name"; 64 | files.sort(function(a,b){ 65 | //按名称排列 66 | if(order=="name"){ 67 | return a - b; 68 | }else{ 69 | //按修改时间顺序排列 70 | return fs.statSync(a).mtime.getTime() - fs.statSync(b).mtime.getTime(); 71 | } 72 | }) 73 | if (!files.length) { 74 | logger.warn('Specified empty list of source SVG files.'); 75 | completeTask(); 76 | return; 77 | } 78 | 79 | // Options 80 | var o = { 81 | logger: logger, 82 | fontBaseName: options.fontname || 'iconfont', 83 | destCss: options.destCss || options.dest, 84 | dest: options.dest, 85 | relativeFontPath: options.relativeFontPath, 86 | addHashes: options.hashes !== false, 87 | addLigatures: options.ligatures === true, 88 | template: options.template, 89 | syntax: options.syntax || 'bem', 90 | templateOptions: options.templateOptions || {}, 91 | stylesheet: options.stylesheet || path.extname(options.template).replace(/^\./, '') || 'css', 92 | htmlDemo: options.htmlDemo !== false, 93 | htmlDemoTemplate: options.htmlDemoTemplate, 94 | styles: optionToArray(options.styles, 'font,icon'), 95 | types: optionToArray(options.types, 'svg,eot,woff,ttf,woff2'), 96 | order: optionToArray(options.order, wf.fontFormats), 97 | embed: options.embed === true ? ['woff'] : optionToArray(options.embed, false), 98 | rename: options.rename || path.basename, 99 | engine: 'node', 100 | autoHint: options.autoHint !== false, 101 | codepoints: options.codepoints, 102 | startCodepoint: options.startCodepoint || wf.UNICODE_PUA_START, 103 | ie7: options.ie7 === true, 104 | normalize: options.normalize === true, 105 | round: options.round !== undefined ? options.round : 10e12, 106 | fontHeight: options.fontHeight !== undefined ? options.fontHeight : 512, 107 | descent: options.descent !== undefined ? options.descent : 64, 108 | cache: options.cache || path.join(__dirname, '..', '.cache'), 109 | callback: options.callback 110 | }; 111 | 112 | o = _.extend(o, { 113 | fontName: o.fontBaseName, 114 | destHtml: options.destHtml || o.destCss, 115 | fontfaceStyles: has(o.styles, 'font'), 116 | baseStyles: has(o.styles, 'icon'), 117 | extraStyles: has(o.styles, 'extra'), 118 | files: files, 119 | glyphs: [] 120 | }); 121 | 122 | // “Rename” files 123 | o.glyphs = o.files.map(function(file) { 124 | return o.rename(file).replace(path.extname(file), ''); 125 | }); 126 | 127 | 128 | // Check or generate codepoints 129 | // @todo Codepoint can be a Unicode code or character. 130 | var currentCodepoint = o.startCodepoint; 131 | if (!o.codepoints) o.codepoints = {}; 132 | o.glyphs.forEach(function(name) { 133 | if (!o.codepoints[name]) { 134 | o.codepoints[name] = getNextCodepoint(); 135 | } 136 | }); 137 | 138 | // Check if we need to generate font 139 | /*o.hash = getHash(); 140 | var previousHash = readHash(this.name, this.target); 141 | logger.verbose('New hash:', o.hash, '- previous hash:', previousHash); 142 | if (o.hash === previousHash) { 143 | logger.verbose('Config and source files weren’t changed since last run, checking resulting files...'); 144 | var regenerationNeeded = false; 145 | 146 | var generatedFiles = wf.generatedFontFiles(o); 147 | if (!generatedFiles.length){ 148 | regenerationNeeded = true; 149 | } 150 | else { 151 | generatedFiles.push(getDemoFilePath()); 152 | generatedFiles.push(getCssFilePath()); 153 | 154 | regenerationNeeded = _.some(generatedFiles, function(filename) { 155 | if (!filename) return false; 156 | if (!fs.existsSync(filename)) { 157 | logger.verbose('File', filename, ' is missed.'); 158 | return true; 159 | } 160 | return false; 161 | }); 162 | } 163 | if (!regenerationNeeded) { 164 | logger.log('Font ' + chalk.cyan(o.fontName) + ' wasn’t changed since last run.'); 165 | completeTask(); 166 | return; 167 | } 168 | }*/ 169 | 170 | // Save new hash and run 171 | //saveHash(this.name, this.target, o.hash); 172 | async.waterfall([ 173 | createOutputDirs, 174 | cleanOutputDir, 175 | generateFont, 176 | generateWoff2Font, 177 | //generateStylesheet, 178 | //generateDemoHtml, 179 | printDone 180 | ], completeTask); 181 | 182 | /** 183 | * Call callback function if it was specified in the options. 184 | */ 185 | function completeTask() { 186 | if (o && _.isFunction(o.callback)) { 187 | o.callback(o.fontName, o.types, o.glyphs); 188 | } 189 | allDone(); 190 | } 191 | 192 | /** 193 | * Calculate hash to flush browser cache. 194 | * Hash is based on source SVG files contents, task options and grunt-webfont version. 195 | * 196 | * @return {String} 197 | */ 198 | function getHash() { 199 | // Source SVG files contents 200 | o.files.forEach(function(file) { 201 | md5.update(fs.readFileSync(file, 'utf8')); 202 | }); 203 | 204 | // Options 205 | md5.update(JSON.stringify(o)); 206 | 207 | // grunt-webfont version 208 | var packageJson = require('../package.json'); 209 | md5.update(packageJson.version); 210 | 211 | return md5.digest('hex'); 212 | } 213 | 214 | /** 215 | * Create output directory 216 | * 217 | * @param {Function} done 218 | */ 219 | function createOutputDirs(done) { 220 | mkdirp.sync(o.destCss); 221 | mkdirp.sync(o.dest); 222 | done(); 223 | } 224 | 225 | /** 226 | * Clean output directory 227 | * 228 | * @param {Function} done 229 | */ 230 | function cleanOutputDir(done) { 231 | var htmlDemoFileMask = path.join(o.destCss, o.fontBaseName + '*.{' + o.stylesheet + ',html}'); 232 | var files = glob.sync(htmlDemoFileMask).concat(wf.generatedFontFiles(o)); 233 | async.forEach(files, function(file, next) { 234 | fs.unlink(file, next); 235 | }, done); 236 | } 237 | 238 | /** 239 | * Generate font using selected engine 240 | * 241 | * @param {Function} done 242 | */ 243 | function generateFont(done) { 244 | var engine = require('./engines/' + o.engine); 245 | engine(o, function(result) { 246 | if (result === false) { 247 | // Font was not created, exit 248 | completeTask(); 249 | } 250 | 251 | if (result) { 252 | o = _.extend(o, result); 253 | } 254 | 255 | done(); 256 | }); 257 | } 258 | 259 | /** 260 | * Converts TTF font to WOFF2. 261 | * 262 | * @param {Function} done 263 | */ 264 | function generateWoff2Font(done) { 265 | if (!has(o.types, 'woff2')) { 266 | done(); 267 | return; 268 | } 269 | 270 | // Run woff2_compress 271 | var ttfFont = wf.getFontPath(o, 'ttf'); 272 | var args = [ 273 | 'woff2_compress', 274 | ttfFont 275 | ]; 276 | 277 | exec(args, function(err, out, code) { 278 | if (err) { 279 | if (err instanceof Error) { 280 | if (err.code === 'ENOENT') { 281 | logger.error('woff2_compress not found. It is required for creating WOFF2 fonts.'); 282 | done(); 283 | return; 284 | } 285 | err = err.message; 286 | } 287 | logger.error('Can’t run woff2_compress.\n\n' + err); 288 | done(); 289 | return; 290 | } 291 | 292 | // Remove TTF font if not needed 293 | if (!has(o.types, 'ttf')) { 294 | fs.unlinkSync(ttfFont); 295 | } 296 | 297 | done(); 298 | }); 299 | } 300 | 301 | /** 302 | * Generate CSS 303 | * 304 | * @param {Function} done 305 | */ 306 | function generateStylesheet(done) { 307 | // Relative fonts path 308 | if (!o.relativeFontPath) { 309 | o.relativeFontPath = path.relative(o.destCss, o.dest); 310 | } 311 | o.relativeFontPath = normalizePath(o.relativeFontPath); 312 | 313 | // Generate font URLs to use in @font-face 314 | var fontSrcs = [[], []]; 315 | o.order.forEach(function(type) { 316 | if (!has(o.types, type)) return; 317 | wf.fontsSrcsMap[type].forEach(function(font, idx) { 318 | if (font) { 319 | fontSrcs[idx].push(generateFontSrc(type, font)); 320 | } 321 | }); 322 | }); 323 | 324 | // Convert them to strings that could be used in CSS 325 | var fontSrcSeparator = option(wf.fontSrcSeparators, o.stylesheet); 326 | fontSrcs.forEach(function(font, idx) { 327 | // o.fontSrc1, o.fontSrc2 328 | o['fontSrc'+(idx+1)] = font.join(fontSrcSeparator); 329 | }); 330 | o.fontRawSrcs = fontSrcs; 331 | 332 | // Convert codepoints to array of strings 333 | var codepoints = []; 334 | _.each(o.glyphs, function(name) { 335 | codepoints.push(o.codepoints[name].toString(16)); 336 | }); 337 | o.codepoints = codepoints; 338 | 339 | // Prepage glyph names to use as CSS classes 340 | o.glyphs = _.map(o.glyphs, classnameize); 341 | 342 | // Read JSON file corresponding to CSS template 343 | var templateJson = readTemplate(o.template, o.syntax, '.json', true); 344 | if (templateJson) o = _.extend(o, JSON.parse(templateJson.template)); 345 | 346 | // Now override values with templateOptions 347 | if (o.templateOptions) o = _.extend(o, o.templateOptions); 348 | 349 | // Generate CSS 350 | var ext = path.extname(o.template) || '.css'; // Use extension of o.template file if given, or default to .css 351 | o.cssTemplate = readTemplate(o.template, o.syntax, ext); 352 | var cssContext = _.extend(o, { 353 | iconsStyles: true 354 | }); 355 | 356 | var css = renderTemplate(o.cssTemplate, cssContext); 357 | 358 | // Fix CSS preprocessors comments: single line comments will be removed after compilation 359 | if (has(['sass', 'scss', 'less', 'styl'], o.stylesheet)) { 360 | css = css.replace(/\/\* *(.*?) *\*\//g, '// $1'); 361 | } 362 | 363 | // Save file 364 | fs.writeFileSync(getCssFilePath(), css); 365 | 366 | done(); 367 | } 368 | 369 | /** 370 | * Generate HTML demo page 371 | * 372 | * @param {Function} done 373 | */ 374 | function generateDemoHtml(done) { 375 | if (!o.htmlDemo) return done(); 376 | 377 | // HTML should not contain relative paths 378 | // If some styles was not included in CSS we should include them in HTML to properly render icons 379 | var relativeRe = new RegExp(_s.escapeRegExp(o.relativeFontPath), 'g'); 380 | var htmlRelativeFontPath = normalizePath(path.relative(o.destHtml, o.dest)); 381 | var context = _.extend(o, { 382 | fontSrc1: o.fontSrc1.replace(relativeRe, htmlRelativeFontPath), 383 | fontSrc2: o.fontSrc2.replace(relativeRe, htmlRelativeFontPath), 384 | fontfaceStyles: true, 385 | baseStyles: true, 386 | extraStyles: false, 387 | iconsStyles: true, 388 | stylesheet: 'css' 389 | }); 390 | var htmlStyles = renderTemplate(o.cssTemplate, context); 391 | var htmlContext = _.extend(context, { 392 | styles: htmlStyles 393 | }); 394 | 395 | // Generate HTML 396 | var demoTemplate = readTemplate(o.htmlDemoTemplate, 'demo', '.html'); 397 | var demo = renderTemplate(demoTemplate, htmlContext); 398 | 399 | // Save file 400 | fs.writeFileSync(getDemoFilePath(), demo); 401 | 402 | done(); 403 | } 404 | 405 | /** 406 | * Print log 407 | * 408 | * @param {Function} done 409 | */ 410 | function printDone(done) { 411 | logger.log('Font ' + chalk.cyan(o.fontName) + ' with ' + o.glyphs.length + ' glyphs created.'); 412 | done(); 413 | } 414 | 415 | 416 | /** 417 | * Helpers 418 | */ 419 | 420 | /** 421 | * Convert a string of comma seperated words into an array 422 | * 423 | * @param {String} val Input string 424 | * @param {String} defVal Default value 425 | * @return {Array} 426 | */ 427 | function optionToArray(val, defVal) { 428 | if (val === undefined) val = defVal; 429 | if (!val) return []; 430 | if (typeof val !== 'string') return val; 431 | if (val.indexOf(',') !== -1) { 432 | return val.split(','); 433 | } 434 | else { 435 | return [val]; 436 | } 437 | } 438 | 439 | /** 440 | * Check if a value exists in an array 441 | * 442 | * @param {Array} haystack Array to find the needle in 443 | * @param {Mixed} needle Value to find 444 | * @return {Boolean} Needle was found 445 | */ 446 | function has(haystack, needle) { 447 | return haystack.indexOf(needle) !== -1; 448 | } 449 | 450 | /** 451 | * Return a specified option if it exists in an object or `_default` otherwise 452 | * 453 | * @param {Object} map Options object 454 | * @param {String} key Option to find in the object 455 | * @return {Mixed} 456 | */ 457 | function option(map, key) { 458 | if (key in map) { 459 | return map[key]; 460 | } 461 | else { 462 | return map._default; 463 | } 464 | } 465 | 466 | /** 467 | * Find next unused codepoint. 468 | * 469 | * @return {Integer} 470 | */ 471 | function getNextCodepoint() { 472 | while (_.contains(o.codepoints, currentCodepoint)) { 473 | currentCodepoint++; 474 | } 475 | return currentCodepoint; 476 | } 477 | 478 | /** 479 | * Check whether file is SVG or not 480 | * 481 | * @param {String} filepath File path 482 | * @return {Boolean} 483 | */ 484 | function isSvgFile(filepath) { 485 | return path.extname(filepath).toLowerCase() === '.svg'; 486 | } 487 | 488 | /** 489 | * Convert font file to data:uri and remove source file 490 | * 491 | * @param {String} fontFile Font file path 492 | * @return {String} Base64 encoded string 493 | */ 494 | function embedFont(fontFile) { 495 | // Convert to data:uri 496 | var dataUri = fs.readFileSync(fontFile, 'base64'); 497 | var type = path.extname(fontFile).substring(1); 498 | var fontUrl = 'data:application/x-font-' + type + ';charset=utf-8;base64,' + dataUri; 499 | 500 | // Remove font file 501 | fs.unlinkSync(fontFile); 502 | 503 | return fontUrl; 504 | } 505 | 506 | /** 507 | * Append a slash to end of a filepath if it not exists and make all slashes forward 508 | * 509 | * @param {String} filepath File path 510 | * @return {String} 511 | */ 512 | function normalizePath(filepath) { 513 | if (!filepath.length) return filepath; 514 | 515 | // Make all slashes forward 516 | filepath = filepath.replace(/\\/g, '/'); 517 | 518 | // Make sure path ends with a slash 519 | if (!_s.endsWith(filepath, '/')) { 520 | filepath += '/'; 521 | } 522 | 523 | return filepath; 524 | } 525 | 526 | /** 527 | * Generate URL for @font-face 528 | * 529 | * @param {String} type Type of font 530 | * @param {Object} font URL or Base64 string 531 | * @return {String} 532 | */ 533 | function generateFontSrc(type, font) { 534 | var filename = template(o.fontName + font.ext, o); 535 | 536 | var url; 537 | if (font.embeddable && has(o.embed, type)) { 538 | url = embedFont(path.join(o.dest, filename)); 539 | } 540 | else { 541 | url = o.relativeFontPath + filename; 542 | if (o.addHashes) { 543 | if (url.indexOf('#iefix') === -1) { // Do not add hashes for OldIE 544 | url = url.replace(/(\.\w+)/, '$1?' + o.hash); 545 | } 546 | } 547 | } 548 | 549 | var src = 'url("' + url + '")'; 550 | if (font.format) src += ' format("' + font.format + '")'; 551 | 552 | return src; 553 | } 554 | 555 | /** 556 | * Reat the template file 557 | * 558 | * @param {String} template Template file path 559 | * @param {String} syntax Syntax (bem, bootstrap, etc.) 560 | * @param {String} ext Extention of the template 561 | * @return {Object} {filename: 'Template filename', template: 'Template code'} 562 | */ 563 | function readTemplate(template, syntax, ext, optional) { 564 | var filename = template 565 | ? path.resolve(template.replace(path.extname(template), ext)) 566 | : path.join(__dirname, 'templates/' + syntax + ext) 567 | ; 568 | if (fs.existsSync(filename)) { 569 | return { 570 | filename: filename, 571 | template: fs.readFileSync(filename, 'utf8') 572 | }; 573 | } 574 | else if (!optional) { 575 | return grunt.fail.fatal('Cannot find template at path: ' + filename); 576 | } 577 | } 578 | 579 | /** 580 | * Render template with error reporting 581 | * 582 | * @param {Object} template {filename: 'Template filename', template: 'Template code'} 583 | * @param {Object} context Template context 584 | * @return {String} 585 | */ 586 | function renderTemplate(template, context) { 587 | try { 588 | return _.template(template.template, context); 589 | } 590 | catch (e) { 591 | grunt.fail.fatal('Error while rendering template ' + template.filename + ': ' + e.message); 592 | } 593 | } 594 | 595 | /** 596 | * Basic template function: replaces {variables} 597 | * 598 | * @param {Template} tmpl Template code 599 | * @param {Object} context Values object 600 | * @return {String} 601 | */ 602 | function template(tmpl, context) { 603 | return tmpl.replace(/\{([^\}]+)\}/g, function(m, key) { 604 | return context[key]; 605 | }); 606 | } 607 | 608 | /** 609 | * Prepare string to use as CSS class name 610 | * 611 | * @param {String} str 612 | * @return {String} 613 | */ 614 | function classnameize(str) { 615 | return _s.trim(str).replace(/\s+/g, '-'); 616 | } 617 | 618 | /** 619 | * Return path of CSS file. 620 | * 621 | * @return {String} 622 | */ 623 | function getCssFilePath() { 624 | var cssFilePrefix = option(wf.cssFilePrefixes, o.stylesheet); 625 | return path.join(o.destCss, cssFilePrefix + o.fontBaseName + '.' + o.stylesheet); 626 | } 627 | 628 | /** 629 | * Return path of HTML demo file or `null` if its generation was disabled. 630 | * 631 | * @return {String} 632 | */ 633 | function getDemoFilePath() { 634 | if (!o.htmlDemo) return null; 635 | return path.join(o.destHtml, o.fontBaseName + '.html'); 636 | } 637 | 638 | /** 639 | * Save hash to cache file. 640 | * 641 | * @param {String} name Task name (webfont). 642 | * @param {String} target Task target name. 643 | * @param {String} hash Hash. 644 | */ 645 | function saveHash(name, target, hash) { 646 | var filepath = getHashPath(name, target); 647 | mkdirp.sync(path.dirname(filepath)); 648 | fs.writeFileSync(filepath, hash); 649 | } 650 | 651 | /** 652 | * Read hash from cache file or `null` if file don’t exist. 653 | * 654 | * @param {String} name Task name (webfont). 655 | * @param {String} target Task target name. 656 | * @return {String} 657 | */ 658 | function readHash(name, target) { 659 | var filepath = getHashPath(name, target); 660 | if (fs.existsSync(filepath)) { 661 | return fs.readFileSync(filepath, 'utf8'); 662 | } 663 | return null; 664 | } 665 | 666 | /** 667 | * Return path to cache file. 668 | * 669 | * @param {String} name Task name (webfont). 670 | * @param {String} target Task target name. 671 | * @return {String} 672 | */ 673 | function getHashPath(name, target) { 674 | return path.join(o.cache, name, target, 'hash'); 675 | } 676 | 677 | } 678 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fis-command-webfont", 3 | "description": "Ultimate SVG to webfont converter for FIS.", 4 | "version": "0.0.2", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/fex-team/fis-command-webfont" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/fex-team/fis-command-webfont/issues" 11 | }, 12 | "licenses": [ 13 | { 14 | "type": "MIT", 15 | "url": "https://github.com/fex-team/fis-command-webfont/blob/master/License.md" 16 | } 17 | ], 18 | "main": "index.js", 19 | "engines": { 20 | "node": ">=0.10.0" 21 | }, 22 | "dependencies": { 23 | "async": "~0.9.0", 24 | "chalk": "~0.5.1", 25 | "exec": "~0.1.2", 26 | "glob": "~4.3.1", 27 | "lodash": "~2.4.1", 28 | "memorystream": "~0.2.0", 29 | "mkdirp": "~0.5.0", 30 | "svg2ttf": "~1.2.0", 31 | "svgicons2svgfont": "1.0.0", 32 | "svgo": "~0.5.0", 33 | "temp": "~0.8.1", 34 | "ttf2eot": "~1.3.0", 35 | "ttf2woff": "~1.3.0", 36 | "underscore.string": "~2.4.0", 37 | "winston": "~0.8.3" 38 | }, 39 | "keywords": [ 40 | "font", 41 | "webfont", 42 | "fontforge", 43 | "font-face", 44 | "woff", 45 | "woff2", 46 | "ttf", 47 | "svg", 48 | "eot", 49 | "truetype", 50 | "css", 51 | "icon" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /test/dest/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fex-team/fis-command-webfont/ac21ddff7c5c02056e8fd87b13b9575c52c2593a/test/dest/icons.eot -------------------------------------------------------------------------------- /test/dest/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/dest/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fex-team/fis-command-webfont/ac21ddff7c5c02056e8fd87b13b9575c52c2593a/test/dest/icons.ttf -------------------------------------------------------------------------------- /test/dest/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fex-team/fis-command-webfont/ac21ddff7c5c02056e8fd87b13b9575c52c2593a/test/dest/icons.woff -------------------------------------------------------------------------------- /test/dest/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fex-team/fis-command-webfont/ac21ddff7c5c02056e8fd87b13b9575c52c2593a/test/dest/icons.woff2 -------------------------------------------------------------------------------- /test/src/i-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /test/src/i-wrong.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /test/src/mailru.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/src/odnoklassniki.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/src/pinterest.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/src/plusone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | plusone 4 | Created with Sketch (http://www.bohemiancoding.com/sketch) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/src/single.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | single 4 | Created with Sketch (http://www.bohemiancoding.com/sketch) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------