├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── renovate.json ├── src ├── App.vue ├── lib │ └── svgFontRenderer.js ├── main.js └── plugins │ └── vuetify.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | cache: npm 6 | 7 | script: npm run build 8 | 9 | deploy: 10 | provider: pages 11 | skip_cleanup: true 12 | github_token: $GITHUB_TOKEN 13 | local_dir: dist 14 | on: 15 | branch: master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jeremias Volker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Single-Line Font Renderer 2 | 3 | ## Text renderer for CNC machines 4 | 5 | This web based tool renders and exports text (currently from SVG format) intended for CNC machines like pen plotters or laser engravers. 6 | 7 | These machines require a different single-line font type than the more commonly used outline fonts (eg. TTF/OTF) which are mainly intended for screens and printing. More on this topic in this [article](https://www.evilmadscientist.com/2011/hershey-text-an-inkscape-extension-for-engraving-fonts/). 8 | 9 | This tool is an attempt to create a browser based alternative to the excellent [Hershey Text Extension for Inkscape](https://wiki.evilmadscientist.com/Hershey_Text) by Evil Mad Scientist. It has a simplified interface and doesn't require installation. On the downside, this tools feature set is currently more limited. 10 | 11 | [**>> CHECK IT OUT HERE <<**](https://jvolker.github.io/single-line-font-renderer/) 12 | 13 | Screenshot of the software 14 | 15 | **Features:** 16 | - latest fonts from the [SVG fonts repository](https://gitlab.com/oskay/svg-fonts) are loaded by default 17 | - loads local SVG font files optionally 18 | - adjust font scale and stroke width 19 | - smoothing/simplification (this is experimental: the results from Inkscape are better in some cases) 20 | - export as SVG file 21 | 22 | **Ideas for furture features:** 23 | - [ ] line breaks 24 | - [ ] Use meaningful units: eg. font size and stroke width in mm 25 | - [ ] support for otf/ttf files (eg. using https://github.com/opentypejs/opentype.js): some single-line fonts are set in these types and hard to use in other software 26 | - [ ] DXF export (eg. using https://github.com/microsoft/maker.js/issues/480 or https://github.com/jscad/io/tree/master/packages/svg-deserializer and https://github.com/jscad/io/tree/master/packages/dxf-serializer) 27 | - [ ] move svg font renderer into separate repo with Node.js support and NPM release 28 | - [ ] text area: words wrap to new line when longer than a certain length 29 | - [ ] alignment: right and center 30 | 31 | ## Export and usage 32 | 33 | There are many different tools and ways to use this with CNC machines depending on the machine and use-case. Single-line font renderer was built with the following workflow in mind: 34 | 35 | 1. This tool exports SVGs to be used and rearranged in a vector/graphic design software like [Affinity Designer](https://affinity.serif.com/en-gb/designer/), Adobe Illustrator or others. 36 | 2. The results can then, for example, be used on an [Axidraw pen plotter](https://axidraw.com/) using [saxi](https://github.com/nornagon/saxi/), a web based control software for Axidraw plotters. 37 | 38 | 39 | ## Fonts 40 | 41 | This tool makes use of SVG fonts. This format has advantages over the original Hershey font format as explained in this [article](https://www.evilmadscientist.com/2019/hershey-text-v30/). The fonts are sourced from a [repositiory with SVG fonts](https://gitlab.com/oskay/svg-fonts) maintained by Evil Mad Scientist. It contains updated Hershey and other fonts converted and shared by a variety of people. Lots of them include more glyphs and therefore better language support than the original fonts. 42 | 43 | This tool uses the latest fonts straight from that repository. Please contribute to that repository to improve the variety of fonts and their language support. 44 | 45 | **Font licenses:** Before using any of the fonts, please make sure to check their license contained in that repository. 46 | 47 | ## Acknowledgement 48 | 49 | The rendering core is heavily borrowed from: https://github.com/techninja/hersheytextjs 50 | Thanks to [Evil Mad Scientist](https://www.evilmadscientist.com/) for their pioneering work in this field. 51 | 52 | ## Alternatives to this tool 53 | - [Hershey Text Extension for Inkscape](https://wiki.evilmadscientist.com/Hershey_Text) by Evil Mad Scientist 54 | - [CNC Text Tool](https://msurguy.github.io/cnc-text-tool/) 55 | - [True single-stroke-font text creator](https://www.templatemaker.nl/singlelinetext/) 56 | 57 | ## Development 58 | 59 | ### Project setup 60 | ``` 61 | npm install 62 | ``` 63 | 64 | ### Compiles and hot-reloads for development 65 | ``` 66 | npm run serve 67 | ``` 68 | 69 | ### Compiles and minifies for production 70 | ``` 71 | npm run build 72 | ``` 73 | 74 | ### Lints and fixes files 75 | ``` 76 | npm run lint 77 | ``` 78 | 79 | ### Customize configuration 80 | See [Configuration Reference](https://cli.vuejs.org/config/). 81 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-line-font-renderer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "0.21.1", 13 | "axios-cache-adapter": "2.5.0", 14 | "cheerio": "1.0.0-rc.5", 15 | "core-js": "3.8.3", 16 | "paper": "0.12.11", 17 | "vue": "2.6.12", 18 | "vuetify": "2.4.3" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "4.5.11", 22 | "@vue/cli-plugin-eslint": "4.5.11", 23 | "@vue/cli-service": "4.5.11", 24 | "babel-eslint": "10.1.0", 25 | "eslint": "7.18.0", 26 | "eslint-plugin-vue": "7.5.0", 27 | "sass": "1.32.5", 28 | "sass-loader": "10.1.1", 29 | "vue-cli-plugin-vuetify": "2.0.9", 30 | "vue-template-compiler": "2.6.12", 31 | "vuetify-loader": "1.6.0" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "eslint:recommended" 41 | ], 42 | "parserOptions": { 43 | "parser": "babel-eslint" 44 | }, 45 | "rules": { 46 | "no-unused-vars": "warn" 47 | } 48 | }, 49 | "browserslist": [ 50 | "> 1%", 51 | "last 2 versions", 52 | "not dead" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvolker/single-line-font-renderer/26a206672dd19dfc4ae2b1b0259183c1eda37700/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Single-Line Font Renderer 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 245 | 246 | 493 | 494 | 495 | -------------------------------------------------------------------------------- /src/lib/svgFontRenderer.js: -------------------------------------------------------------------------------- 1 | // This is taken and quickly adapted from: https://github.com/techninja/hersheytextjs/blob/master/lib/hersheytext.js 2 | 3 | 4 | 5 | 6 | /** 7 | * @file HersheyText node module for easily including the letter data or rendering text. 8 | */ 9 | const cheerio = require('cheerio'); 10 | // const fs = require('fs'); 11 | // const path = require('path'); 12 | 13 | // exports.fonts = require('../hersheytext.min.json'); 14 | // exports.svgFonts = require('../svg_fonts/index.json'); 15 | 16 | exports.svgFonts = [] 17 | 18 | // Rewrite internal reference filename for SVG fonts to the absolute path. 19 | // Object.entries(exports.svgFonts).forEach(([key, item]) => { 20 | // item.file = path.join(__dirname, '..', 'svg_fonts', item.file); 21 | // }); 22 | 23 | /** 24 | * Helper to load font data from SVG fonts or original Hershey JSON data. 25 | * 26 | * @param {string} fontName 27 | * The machine name/id of the font, either from the Hershey JSON key, or the 28 | * SVG font index JSON key. 29 | * 30 | * @returns {object} 31 | * Object containing all chars supported by the font 32 | */ 33 | function getFontData(fontName) { 34 | const font = { 35 | info: 'Invalid Font', 36 | getChar: () => null, 37 | }; 38 | 39 | // if (exports.fonts[fontName]) { 40 | // font.type = 'hershey'; 41 | // font.info = { 42 | // 'font-family': exports.fonts[fontName].name, // Font title. 43 | // 'units-per-em': 10, // Height. 44 | // 'horiz-adv-x': 10, // Space/Default width. 45 | // }; 46 | // font.chars = exports.fonts[fontName].chars; 47 | 48 | // // Get font characters via ascii index offset. 49 | // font.getChar = (char) => { 50 | // const data = font.chars[char.charCodeAt(0) - 33]; 51 | // if (data) { 52 | // return { type: char, name: char, width: parseInt(data.o, 10), d: data.d }; 53 | // } 54 | // return null; 55 | // }; 56 | 57 | // } else if (exports.svgFonts[fontName]) { 58 | // const data = fs.readFileSync(exports.svgFonts[fontName].file); 59 | const data = exports.svgFonts[fontName].data; 60 | const $ = cheerio.load(data, { xmlMode: true }); 61 | 62 | font.type = 'svg'; 63 | font.info = { ...$('font-face').attr(), ...$('font').attr() }; 64 | font.chars = {}; 65 | 66 | $('glyph').each((index) => { 67 | const item = $('glyph')[index]; 68 | // Add all glyphs including space (which only contains the space width!). 69 | font.chars[item.attribs.unicode] = { 70 | type: item.attribs.unicode, 71 | name: item.attribs['glyph-name'], 72 | width: parseFloat(item.attribs['horiz-adv-x']) || parseFloat(font.info['horiz-adv-x']), 73 | d: item.attribs.d || null, 74 | } 75 | }); 76 | 77 | // Get font characters directly via unicode object keys. 78 | font.getChar = char => font.chars[char]; 79 | // } 80 | 81 | return font; 82 | } 83 | 84 | // Export the get data function. 85 | exports.getFontData = getFontData; 86 | 87 | /** 88 | * Add a new SVG Font via file location. 89 | * 90 | * @returns {boolean} 91 | * True if it worked, false if not. 92 | */ 93 | // exports.addSVGFont = (file) => { 94 | // try { 95 | // const data = fs.readFileSync(file); 96 | // const $ = cheerio.load(data, { xmlMode: true }); 97 | // const name = $('font-face').attr('font-family'); 98 | // const cleanName = name.toLowerCase().replace(/\s/g, '_'); 99 | // exports.svgFonts[cleanName] = { name, file }; 100 | // } catch (error) { 101 | // console.error(error); 102 | // return false; 103 | // } 104 | // }; 105 | 106 | exports.addSVGFontFromData = (data) => { 107 | try { 108 | const $ = cheerio.load(data, { xmlMode: true }); 109 | const name = $('font-face').attr('font-family'); 110 | const cleanName = name.toLowerCase().replace(/\s/g, '_'); 111 | exports.svgFonts[cleanName] = { name, data }; 112 | return cleanName; 113 | } catch (error) { 114 | console.error(error); 115 | return false; 116 | } 117 | }; 118 | 119 | /** 120 | * Get a flat array list of machine name valid fonts. 121 | * 122 | * @returns {array} 123 | * Flat array of font name/id strings. 124 | */ 125 | // exports.getFonts = () => [...Object.keys(exports.fonts), ...Object.keys(exports.svgFonts)]; 126 | 127 | /** 128 | * Render a string of text in a Hershey engraving font to SVG XML. 129 | * 130 | * @param {string} text 131 | * Text string to be rendered 132 | * @param {object} rawOptions 133 | * Object of named options: 134 | * font {string}: Name of font face from main font object 135 | * id {string}: ID to give the final g(roup) SVG DOM object 136 | * pos {object}: {x, y} object of where to position the object 137 | * charSpacingAdjust {int}: Amount to add or remove from between char spacing. 138 | * charHeightAdjust {int}: Amount to add or remove from line height. 139 | * scale {int}: Scale to multiply size of everything by 140 | * 141 | * @returns {string|boolean} 142 | * Internal SVG content to be rendered as you see fit. 143 | * Returns === false if error. 144 | */ 145 | exports.renderTextSVG = function(text, rawOptions = {}) { 146 | const options = { 147 | id: 'text', 148 | font: 'futural', 149 | charSpacingAdjust: 0, 150 | charHeightAdjust: 0, 151 | scale: 2, 152 | pos: { x: 0, y: 0 }, 153 | ...rawOptions, 154 | }; 155 | 156 | // Prep SVG export. 157 | const $ = cheerio.load('', { xmlMode: true }); // Initial DOM 158 | 159 | try { 160 | const font = getFontData(options.font); 161 | // console.log(font.type) 162 | const multiplyer = font.type === 'svg' ? 1 : 1.68; 163 | const offset = { left: 0, top: 0 }; 164 | 165 | // Create central group 166 | const $textGroup = $('g'); 167 | $textGroup.attr({ 168 | id: options.id, 169 | stroke: 'black', 170 | fill: 'none', 171 | transform: 172 | `scale(${options.scale}) translate(${options.pos.x}, ${options.pos.y})` 173 | }); 174 | 175 | // Initial Line container 176 | const lineCount = 0; 177 | const $groupLine = $('').attr('id', options.id + '-line-' + lineCount); 178 | $textGroup.prepend($groupLine); 179 | 180 | // Move through each word 181 | const words = text.split(' '); 182 | for(let w in words) { 183 | const word = words[w]; 184 | 185 | // Move through each letter 186 | for(let i in word) { 187 | 188 | const rawChar = word[i]; 189 | const char = font.getChar(rawChar); 190 | 191 | // Only print in range chars 192 | if (char) { 193 | const $path = $('').attr({ 194 | d: char.d, 195 | stroke: 'black', 196 | 'stroke-width': 1, 197 | fill: 'none', 198 | transform: `translate(${offset.left}, ${offset.top})`, 199 | letter: word[i] 200 | }); 201 | 202 | if (font.type === 'svg') { 203 | $path.attr('transform', `translate(${offset.left}, ${font.info['units-per-em']}) scale(1, -1)`); 204 | } 205 | 206 | // Add the char to the DOM group. 207 | $groupLine.append($path).append($('')); 208 | 209 | // Position next character. 210 | offset.left += (char.width * multiplyer) + options.charSpacingAdjust; 211 | } 212 | } 213 | 214 | // Word boundary: Add a space. 215 | offset.left+= parseInt(font.info['horiz-adv-x'], 10) + options.charSpacingAdjust; 216 | } 217 | 218 | } catch(e) { 219 | console.error(e); 220 | return false; // Error! 221 | } 222 | 223 | return $.html(); // We should be all good! 224 | } 225 | 226 | /** 227 | * Render a string of text in a Hershey engraving font to an array of paths. 228 | * 229 | * @param {string} text 230 | * Text string to be rendered 231 | * @param {object} options 232 | * Object of named options: 233 | * font {string}: [Optional] Name of font face from main font object 234 | * 235 | * @returns {string|boolean} 236 | * Array of font matches for letters in order, returns === false if error. 237 | */ 238 | // exports.renderTextArray = function(text, rawOptions = {}) { 239 | // const out = []; 240 | // const options = { font: 'futural', ...rawOptions }; 241 | 242 | // // Change CRLF with just LF. 243 | // text = text.replace(/\r\n/g, "\n"); 244 | // try { 245 | // const font = getFontData(options.font); 246 | 247 | // // Move through each letter. 248 | // for(let i in text) { 249 | // const char = font.getChar(text[i]); 250 | 251 | // // Only print in range chars. 252 | // if (char){ 253 | // out.push(char); 254 | // } else if (text[i] === "\n") { 255 | // out.push({ name: 'newline', width: 0 }); 256 | // } else { 257 | // // Add a space if char not found. 258 | // // TODO: Add support for "missing-glyph". 259 | // // TODO: This might break for hershey font. Please use SVG font, thank you! 260 | // out.push(font.getChar(' ')); 261 | // } 262 | // } 263 | // } catch(e) { 264 | // console.error(e); 265 | // return false; // Error! 266 | // } 267 | 268 | // return out; 269 | // } 270 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import vuetify from './plugins/vuetify'; 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | vuetify, 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transpileDependencies": [ 3 | "vuetify" 4 | ], 5 | "publicPath" : process.env.NODE_ENV === 'production' 6 | ? '/single-line-font-renderer/' 7 | : '/' 8 | } --------------------------------------------------------------------------------