From a6f397a8f826678d92cd998079402747f74b5007 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Sat, 16 Oct 2021 01:08:58 +0200 Subject: [PATCH] added stroke font creator --- .../stroke_font_creator/edit_stroke_font.inx | 54 ++ .../stroke_font_creator/edit_stroke_font.py | 89 +++ .../gen_stroke_font_data.inx | 40 + .../gen_stroke_font_data.py | 268 +++++++ .../stroke_font_creator/meta.json | 20 + .../render_stroke_font_text.inx | 97 +++ .../render_stroke_font_text.py | 247 ++++++ .../stroke_font_creator/stroke_font_common.py | 428 +++++++++++ .../stroke_font_manager.py | 711 ++++++++++++++++++ .../stroke_font_creator/stroke_font_templ.inx | 49 ++ .../stroke_font_creator/stroke_font_templ.py | 136 ++++ .../strokefontdata/Custom-Script.svg | 1 + .../strokefontdata/Custom-Square Italic.svg | 1 + .../strokefontdata/Custom-Square Normal.svg | 1 + .../strokefontdata/Hershey-Astrology.svg | 1 + .../strokefontdata/Hershey-Cyrillic.svg | 1 + .../strokefontdata/Hershey-Gothic English.svg | 1 + .../strokefontdata/Hershey-Gothic German.svg | 1 + .../strokefontdata/Hershey-Gothic Italian.svg | 1 + .../strokefontdata/Hershey-Greek 1-stroke.svg | 1 + .../strokefontdata/Hershey-Greek medium.svg | 1 + .../strokefontdata/Hershey-Japanese.svg | 1 + .../strokefontdata/Hershey-Markers.svg | 1 + .../strokefontdata/Hershey-Math (lower).svg | 1 + .../strokefontdata/Hershey-Math (upper).svg | 1 + .../strokefontdata/Hershey-Meteorology.svg | 1 + .../strokefontdata/Hershey-Music.svg | 1 + .../strokefontdata/Hershey-Sans 1-stroke.svg | 1 + .../strokefontdata/Hershey-Sans bold.svg | 1 + .../Hershey-Script 1-stroke (alt).svg | 1 + .../Hershey-Script 1-stroke.svg | 1 + .../strokefontdata/Hershey-Script medium.svg | 1 + .../Hershey-Serif bold italic.svg | 1 + .../strokefontdata/Hershey-Serif bold.svg | 1 + .../Hershey-Serif medium italic.svg | 1 + .../strokefontdata/Hershey-Serif medium.svg | 1 + .../strokefontdata/Hershey-Symbolic.svg | 1 + .../strokefontdata/OFL.txt | 97 +++ .../stroke_font_creator/sync_stroke_font.inx | 16 + .../stroke_font_creator/sync_stroke_font.py | 41 + 40 files changed, 2319 insertions(+) create mode 100644 extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.inx create mode 100644 extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.py create mode 100644 extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.inx create mode 100644 extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.py create mode 100644 extensions/fablabchemnitz/stroke_font_creator/meta.json create mode 100644 extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.inx create mode 100644 extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.py create mode 100644 extensions/fablabchemnitz/stroke_font_creator/stroke_font_common.py create mode 100644 extensions/fablabchemnitz/stroke_font_creator/stroke_font_manager.py create mode 100644 extensions/fablabchemnitz/stroke_font_creator/stroke_font_templ.inx create mode 100644 extensions/fablabchemnitz/stroke_font_creator/stroke_font_templ.py create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Custom-Script.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Custom-Square Italic.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Custom-Square Normal.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Astrology.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Cyrillic.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Gothic English.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Gothic German.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Gothic Italian.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Greek 1-stroke.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Greek medium.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Japanese.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Markers.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Math (lower).svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Math (upper).svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Meteorology.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Music.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Sans 1-stroke.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Sans bold.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Script 1-stroke (alt).svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Script 1-stroke.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Script medium.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Serif bold italic.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Serif bold.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Serif medium italic.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Serif medium.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/Hershey-Symbolic.svg create mode 100644 extensions/fablabchemnitz/stroke_font_creator/strokefontdata/OFL.txt create mode 100644 extensions/fablabchemnitz/stroke_font_creator/sync_stroke_font.inx create mode 100644 extensions/fablabchemnitz/stroke_font_creator/sync_stroke_font.py diff --git a/extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.inx b/extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.inx new file mode 100644 index 00000000..787e54a4 --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.inx @@ -0,0 +1,54 @@ + + + Custom Stroke Font - Edit Stroke Font + fablabchemnitz.de.stroke_font_creator.edit_stroke_font + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + 1000 + + + + + + + + path + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.py b/extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.py new file mode 100644 index 00000000..a920da4e --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/edit_stroke_font.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +''' +Inkscape extension to edit a stroke font +Dependencies: stroke_font_common.py and stroke_font_manager.py + +Copyright (C) 2019 Shrinivas Kulkarni + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' + +import inkex +from inkex import Effect, addNS +import sys, os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from stroke_font_common import CommonDefs, InkscapeCharDataFactory, createTempl, getAddFnTypes +from stroke_font_common import getTranslatedPath, formatStyle, getEtree, runEffect +from stroke_font_manager import FontData, xPath, xGlyphName + +class EditStrokeFont(Effect): + + def __init__(self): + Effect.__init__(self) + + addFn, typeFloat, typeInt, typeString, typeBool = getAddFnTypes(self) + + addFn( "--fontName", action = "store", type = typeString, dest = "fontName", \ + default = 'Script', help = "The custom font to edit") + + addFn('--rowCnt', action = 'store', type = typeInt, dest = 'rowCnt', \ + default = '5', help = 'Number of rows (horizontal guides) in the template') + + addFn('--fontSize', action = 'store', type = typeInt, dest = 'fontSize', \ + default = '100', help = 'Size of the source glyphs to be rendered') + + addFn("--tab", action = "store", type = typeString, dest = "tab", \ + default = "sampling", help = "Tab") + + def addElem(self, templLayer, editLayer, glyphIdx, posX, posY): + char = self.fontChars[glyphIdx] + charData = self.strokeFontData.glyphMap[char] + + d = getTranslatedPath(charData.pathStr, posX, posY) + + attribs = {'id':char, 'style':formatStyle(self.charStyle), \ + xPath:d, xGlyphName: charData.glyphName} + getEtree().SubElement(editLayer, addNS('path','svg'), attribs) + + return charData.rOffset + + + def effect(self): + rowCnt = self.options.rowCnt + fontName = self.options.fontName + fontSize = self.options.fontSize + + lineT = CommonDefs.lineT * fontSize + strokeWidth = 0.02 * fontSize + self.charStyle = { 'stroke': '#000000', 'fill': 'none', \ + 'stroke-width':strokeWidth, 'stroke-linecap':'round', \ + 'stroke-linejoin':'round'} + + vgScaleFact = CommonDefs.vgScaleFact + + extPath = os.path.dirname(os.path.abspath(__file__)) + self.strokeFontData = FontData(extPath, fontName, fontSize, \ + InkscapeCharDataFactory()) + + self.fontChars = sorted(self.strokeFontData.glyphMap.keys()) + + glyphCnt = len(self.fontChars) + + createTempl(self.addElem, self, self.strokeFontData.extraInfo, rowCnt, \ + glyphCnt, vgScaleFact, True, lineT) + +runEffect(EditStrokeFont()) diff --git a/extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.inx b/extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.inx new file mode 100644 index 00000000..9bfa6adb --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.inx @@ -0,0 +1,40 @@ + + + Custom Stroke Font - Generate Font Data + fablabchemnitz.de.stroke_font_creator.stroke_font_templ + + + + + + + + + 0 + + + + + + + path + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.py b/extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.py new file mode 100644 index 00000000..1fbaf0a8 --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/gen_stroke_font_data.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 + +''' +Inkscape extension to generate the data for the stroke font glyphs +designed in the current SVG. The current SVG must be generated with the +'Create Font Design Template' extension + +The data generated by this effect is used by the 'Render Text' extension, +to render text with the selected stroke font. + +Copyright (C) 2019 Shrinivas Kulkarni + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' + +import inkex, sys, os, re, math +from bezmisc import bezierlengthSimpson + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from stroke_font_common import InkscapeCharData, CommonDefs, runEffect, getCubicSuperPath, \ + InkscapeCharDataFactory, syncFontList, getAddFnTypes, getParsedPath, getTransformMat, \ + applyTransform, getCubicBoundingBox, formatSuperPath, getCubicLength, getCubicSuperPath +from stroke_font_manager import FontData, xGlyphName, xAscent, \ + xDescent, xCapHeight, xXHeight, xSpaceROff, xFontId, xSize, getDefaultExtraInfo + + +def getNearestGuide(guides, minVal, coIdx, hSeq = None): + if(guides == None or len(guides) == 0): + return None, None + + if(hSeq != None): + guides = [g for g in guides if int(g[0].get(CommonDefs.idAttribName).split('_')[1]) == hSeq] + + if(len(guides) == 1): + return guides[0] + + for i, guide in enumerate(guides): + pp = guide[1] + #pp format [['M',[x1,y1]],['L',[x2,y2]]] + diff = abs(pp[0][1][coIdx] - minVal) + + if(i > 0 and diff > minDiff): + return guides[i-1] + + minDiff = diff + + return guides[-1] + +def getFontSizeFromGuide(pp, vgScaleFact): + #pp format [['M',[x1,y1]],['L',[x2,y2]]] + lHeight = abs(float(pp[1][1][1]) - pp[0][1][1]) + return round(lHeight / vgScaleFact, 2) + +#Apply transform attribute (only straight lines, no arcs etc.) +def transformedParsedPath(elem): + pp = getParsedPath(elem.get('d')) + return pp + + # Following bloc takes care of the special condition where guides are transformed + # TODO: Make it working for 1.0 + + # ~ try: + # ~ transf = elem.get('transform') + # ~ mat = parseTransform(transf) + + # ~ #pp format [['M',[x1,y1]],['L',[x2,y2]]] + # ~ for dElem in pp: + # ~ for i in range(1, len(dElem)): + # ~ param = dElem[i] + # ~ t1 = [[param[x], param[x+1]] for x in range(0, len(param), 2)] + # ~ for t1Elem in t1: + # ~ simpletransform.applyTransformToPoint(mat, t1Elem) + # ~ dElem[i] = [x for l in t1 for x in l] + # ~ elem.set('d', simplepath.formatPath(pp)) + # ~ except: + # ~ #Don't break + # ~ pass + + # ~ return pp + +def updateFontData(strokeFontData, glyphPathElems, hGuides, lvGuides, rvGuides, rightOffsetType): + for elem in glyphPathElems: + char = elem.get(CommonDefs.idAttribName) + path = getCubicSuperPath(elem.get('d')) + + glyphName = elem.get(xGlyphName) + if(glyphName == None): glyphName = char + + #Just in case... + transf = elem.get('transform') + mat = getTransformMat(transf) + applyTransform(mat, path) + + xmin, xmax, ymin, ymax = getCubicBoundingBox(path) + + #Nearest to the bottom (ymax) + hg = getNearestGuide(hGuides, ymax, 1) + hseq = int(hg[0].get(CommonDefs.idAttribName).split('_')[1]) + hgp = hg[1] + #hgp format: [['M',[x1,y1]],['H',[x2,y2]]] + hgY = hgp[0][1][1] + + #Nearest to the left edge (xmin) + lvg = getNearestGuide(lvGuides, xmin, 0, hseq) + lvgp = lvg[1] + #lvgp format: [['M',[x1,y1]],['V',[x2,y2]]] + lvgX = lvgp[0][1][0] + + rvgX = None + if(rvGuides != None and len(rvGuides) > 0): + #Nearest to the right edge (xmax) + rvg = getNearestGuide(rvGuides, xmax, 0, hseq) + rvgp = rvg[1] + #rvgp format: [['M',[x1,y1]],['V',[x2,y2]]] + rvgX = rvgp[0][1][0] + + npath = getCubicSuperPath() + + maxLenSp = None + maxSpLen = 0 + + for subpath in path: + nsub = [] + spLen = 0 + for seg in subpath: + nseg = [] + for pt in seg: + x = round(pt[0] - lvgX, 2) + y = round(pt[1] - hgY, 2) + nseg.append([round(x, 4), round(y, 4)]) + nsub.append(nseg) + npath.append(nsub) + + #Calculate length only if needed + if(rightOffsetType == 'lastNode'): + spLen = getCubicLength(npath) + if(spLen > maxSpLen): + maxSpLen = spLen + maxLenSp = subpath + + if(rightOffsetType == 'lastNode'): + lastNode = maxLenSp[-1][-1] + rOffset = lastNode[0] - lvgX + elif(rvgX != None): + rOffset = rvgX - lvgX + else: + rOffset = xmax - lvgX + + rOffset = round(rOffset, 2) + + pathStr = formatSuperPath(npath) + strokeFontData.updateGlyph(char, rOffset, pathStr, glyphName) + +class GenStrokeFontData(inkex.Effect): + + def __init__(self): + inkex.Effect.__init__(self) + + addFn, typeFloat, typeInt, typeString, typeBool = getAddFnTypes(self) + + addFn('--fontName', action = 'store', type = typeString, dest = 'fontName', \ + help = 'Name of the font to be created') + + addFn('--rightOffsetType', action = 'store', type = typeString, \ + dest = 'rightOffsetType', help = 'Calculation of the right offset of the glyph') + + addFn('--spaceWidth', action = 'store', type = typeFloat, dest = 'spaceWidth', \ + help = 'Space width (enter only if changed') + + addFn('--crInfo', action = 'store', type = typeString, dest = 'crInfo', \ + help = 'Copyright and license details') + + addFn("--tab", action = "store", type = typeString, dest = "tab", \ + default = "sampling", help = "Tab") + + def getGuides(self, idName, idVal): + return [(pn, transformedParsedPath(pn)) for pn in self.document.xpath('//svg:path', \ + namespaces = inkex.NSS) if pn.get(idName) != None and \ + pn.get(idName).startswith(idVal)] + + def getFontExtraInfo(self): + info = {} + nodes = [node for node in self.document.xpath('//svg:' + CommonDefs.fontOtherInfo, \ + namespaces = inkex.NSS)] + if(len(nodes) > 0): + try: + node = nodes[0] + info[xAscent] = float(node.get(xAscent)) + info[xDescent] = float(node.get(xDescent)) + info[xCapHeight] = float(node.get(xCapHeight)) + info[xXHeight] = float(node.get(xXHeight)) + info[xSpaceROff] = float(node.get(xSpaceROff)) + info[xSize] = float(node.get(xSize)) + info[xFontId] = node.get(xFontId) + return info + except: + pass + return None + + def effect(self): + fontName = self.options.fontName + rightOffsetType = self.options.rightOffsetType + crInfo = self.options.crInfo + spaceWidth = self.options.spaceWidth + + #Guide is a tuple of xml elem and parsed path + hGuides = self.getGuides(CommonDefs.idAttribName, CommonDefs.hGuideIDPrefix) + + hGuides = sorted(hGuides, + key = lambda p: int(p[0].get(CommonDefs.idAttribName).split('_')[1])) + + lvGuides = self.getGuides(CommonDefs.idAttribName, CommonDefs.lvGuideIDPrefix) + lvGuides = sorted(lvGuides, key = lambda p: (p[0].get(CommonDefs.idAttribName))) + + rvGuides = self.getGuides(CommonDefs.idAttribName, CommonDefs.rvGuideIDPrefix) + rvGuides = sorted(rvGuides, key = lambda p: (p[0].get(CommonDefs.idAttribName))) + + if(len(lvGuides) == 0 or len(hGuides) == 0): + inkex.errormsg("Missing guides. Please use the Create Font Design " + \ + "Template extension to design the font.") + return + + extraInfo = self.getFontExtraInfo() + if(extraInfo == None): + fontSize = getFontSizeFromGuide(lvGuides[0][1], CommonDefs.vgScaleFact) + extraInfo = getDefaultExtraInfo(fontName, fontSize) + + fontSize = extraInfo[xSize] + + if(round(spaceWidth, 1) == 0): + spaceWidth = extraInfo[xSpaceROff] + if(round(spaceWidth, 1) == 0): spaceWidth = fontSize / 2 + + extraInfo[xSpaceROff] = spaceWidth + + extPath = os.path.dirname(os.path.abspath(__file__)) + strokeFontData = FontData(extPath, fontName, fontSize, InkscapeCharDataFactory()) + + strokeFontData.setCRInfo(crInfo) + strokeFontData.setExtraInfo(extraInfo) + + glyphPaths = [p for p in self.document.xpath('//svg:path', namespaces=inkex.NSS) \ + if (len(p.get(CommonDefs.idAttribName)) == 1)] + + updateFontData(strokeFontData, glyphPaths, hGuides, lvGuides, rvGuides, rightOffsetType) + + strokeFontData.updateFontXML() + + syncFontList(extPath) + +try: + runEffect(GenStrokeFontData()) +except: + inkex.errormsg('The data was not generated due to an error. ' + \ + 'If you are creating non-english glyphs then save the document, re-open and' + \ + 'try generating the font data once again.') diff --git a/extensions/fablabchemnitz/stroke_font_creator/meta.json b/extensions/fablabchemnitz/stroke_font_creator/meta.json new file mode 100644 index 00000000..0cd9df26 --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/meta.json @@ -0,0 +1,20 @@ +[ + { + "name": "Custom Stroke Font - ", + "id": "fablabchemnitz.de.stroke_font_creator.", + "path": "stroke_font_creator", + "original_name": "", + "original_id": "khema.stroke.fnt.gen.", + "license": "GNU GPL v2", + "license_url": "https://github.com/Shriinivas/inkscapestrokefont/blob/master/LICENSE", + "comment": "", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/stroke_font_creator", + "fork_url": "https://github.com/Shriinivas/inkscapestrokefont", + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Stroke+Font+Creator", + "inkscape_gallery_url": null, + "main_authors": [ + "github.com/Shriinivas", + "github.com/vmario89" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.inx b/extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.inx new file mode 100644 index 00000000..8940d0d8 --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.inx @@ -0,0 +1,97 @@ + + + Custom Stroke Font - Render Text + fablabchemnitz.de.stroke_font_creator.render_text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + 1 + 1 + 1.5 + true + 5 + + + + + + + + + + + + + + + + + + 1 + + + + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.py b/extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.py new file mode 100644 index 00000000..4c6dca50 --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/render_stroke_font_text.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 + +""" +Inkscape extension to render text with the stroke fonts. + +The path data used to render the text is generated by the +'Generate Font Data' extension + +The original concept is from Hershey Text extension (Copyright 2011, Windell H. Oskay), +that comes bundled with Inkscape + +This tool extends it with a number of rendering options like flow in boxes, +text alignment and char & line spacing + +Copyright 2019 Shrinivas Kulkarni + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + +from inkex import addNS, NSS, Effect, errormsg +import os, sys +from simplepath import parsePath, translatePath, formatPath + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from stroke_font_common import CommonDefs, InkscapeCharDataFactory, getEtree, getAddFnTypes +from stroke_font_common import computePtInNode, getDecodedChars +from stroke_font_common import getViewCenter, runEffect, getSelectedElements +from stroke_font_common import formatStyle, getCharStyle, getTranslatedPath, getCurrentLayer + +from stroke_font_manager import DrawContext + +class InkscapeFontRenderer: + def __init__(self, layer, vc, strokeWidth): + self.layer = layer + self.g = None + self.currG = None + self.vc = computePtInNode(vc, layer) + + self.strokeWidth = strokeWidth + self.box = None + + def renderChar(self, charData, x, y, naChar): + d = charData.pathStr + style = getCharStyle(self.strokeWidth, naChar) + + d = getTranslatedPath(d, x, y) + + attribs = {'style':formatStyle(style), 'd':d} + getEtree().SubElement(self.currG, addNS('path','svg'), attribs) + + def beforeRender(self): + self.g = getEtree().SubElement(self.layer, 'g') + self.currG = self.g + + def newBoxToBeRendered(self, box, addPlane): + boxG = getEtree().SubElement(self.layer, 'g') + + if('transform' in box.attrib): + boxG.set('transform', box.get('transform')) + + box.getparent().remove(box) + + # order important! + self.g.append(box) + self.g.append(boxG) + self.currG = boxG + self.box = box + + def moveBoxInYDir(self, moveBy): + t = 'translate(' + str(0) + ',' + str(moveBy) + ')' + self.currG.set('transform', t) + + def centerInView(self, width, height): + t = 'translate(' + str(self.vc[0] - width) + ',' + str(self.vc[1] - height) + ')' + self.g.set('transform', t) + + def renderPlainText(self, text, size, x = None, y = None, objName = None): + textStyle = {'fill':'#000000', 'fill-opacity':'1'} + textStyle['font-size'] = str(size) + attribs = {'style':formatStyle(textStyle)} + if(x != None and y != None): + attribs['transform'] = 'translate(' + str(x) + ', '+ str(y)+')' + + textElem = getEtree().SubElement(self.g, addNS('text','svg'), attribs) + textElem.text = text + + def getBoxLeftTopRightBottom(self, box): + x1 = float(box.get('x')) + y1 = float(box.get('y')) + w = float(box.get('width')) + h = float(box.get('height')) + + x2 = w + x1 + y2 = h + y1 + + return x1, y1, x2, y2 + + def getBoxFromCoords(self, x1, y1, x2, y2): + attribs = {'x':str(x1), 'y':str(y1), 'width':str(x2-x1), 'height':str(y2-y1)} + attribs['style'] = self.box.get('style') + self.box = getEtree().SubElement(self.layer, addNS('rect','svg'), attribs) + return self.box + + def getDefaultStartLocation(self): + return 0, 0 + +class RenderStrokeFontText(Effect): + def __init__( self ): + Effect.__init__( self ) + + addFn, typeFloat, typeInt, typeString, typeBool = getAddFnTypes(self) + + addFn( '--tab', action = 'store', type = typeString, dest = 'tab', \ + default = 'splash', help = 'The active tab when Apply was pressed') + + addFn( '--action', action = 'store', type = typeString, dest = 'action', \ + default = 'render', help = 'The active option when Apply was pressed' ) + + addFn( '--text', action = 'store', type = typeString, dest = 'text', \ + default = 'Hello World', help = 'The input text to render') + + addFn( '--filePath', action = 'store', type = typeString, dest = 'filePath', \ + default = '', help = 'Complete path of the text file') + + addFn( '--fontName', action = 'store', type = typeString, dest = 'fontName', \ + default = 'Script', help = 'The custom font to be used for rendering') + + addFn('--fontSize', action = 'store', type = typeFloat, dest = 'fontSize', \ + default = '100', help = 'Size of the font') + + addFn('--charSpacing', action = 'store', type = typeFloat, dest = 'charSpacing', \ + default = '1', help = 'Spacing between characters') + + addFn('--wordSpacing', action = 'store', type = typeFloat, dest = 'wordSpacing', \ + default = '1', help = 'Spacing between words') + + addFn('--lineSpacing', action = 'store', type = typeFloat, dest = 'lineSpacing', \ + default = '1.5', help = 'Spacing between the lines') + + addFn('--flowInBox', action = 'store', type = typeBool, dest = 'flowInBox', \ + default = False, help = 'Fit the text in the selected rectangle objects') + + addFn('--margin', action = 'store', type = typeFloat, dest = 'margin', default = '.1', \ + help = 'Inside margin of text within the box') + + addFn( '--hAlignment', action='store', type = typeString, dest = 'hAlignment', \ + default = 'left', help='Horizontal text alignment within the box') + + addFn( '--vAlignment', action='store', type = typeString, dest = 'vAlignment', \ + default = 'none', help='Vertical text alignment within the box') + + addFn( '--expandDir', action='store', type = typeString, dest = 'expandDir', \ + default = 'x', help='Create new rectangles if text doesn\'t fit the selected ones') + + addFn('--expandDist', action = 'store', type = typeFloat, dest = 'expandDist', \ + default = True, help = 'Offset distance between the newly created rectangles') + + def effect(self): + fontName = self.options.fontName + fontSize = self.options.fontSize + filePath = self.options.filePath + action = self.options.action + + if(action == "renderTable"): + charSpacing = 1 + wordSpacing = 1 + lineSpacing = 2 + else: + charSpacing = self.options.charSpacing + wordSpacing = self.options.wordSpacing + lineSpacing = self.options.lineSpacing + + flowInBox = self.options.flowInBox + margin = self.options.margin + hAlignment = self.options.hAlignment + vAlignment = self.options.vAlignment + expandDir = self.options.expandDir + expandDist = self.options.expandDist + + if(expandDir == 'none'): + expandDir = None + expandDist = None + + extPath = os.path.dirname(os.path.abspath(__file__)) + + strokeWidth = 0.02 * fontSize + + layer = getCurrentLayer(self) + renderer = InkscapeFontRenderer(layer, getViewCenter(self), strokeWidth) + + context = DrawContext(extPath, fontName, fontSize, \ + charSpacing, wordSpacing, lineSpacing, InkscapeCharDataFactory(), renderer) + + if(not context.fontHasGlyphs()): + errormsg('No font data; please select a font and ' + \ + 'ensure that the list is synchronized') + return + + if(action == "renderTable"): + context.renderGlyphTable() + return + + if(action == "renderText"): + text = self.options.text + text = text.replace('\\n','\n').replace('\\\n','\\n') + text = getDecodedChars(text) + + elif(action == "renderFile"): + try: + readmode = 'rU' if CommonDefs.pyVer == 2 else 'r' + with open(filePath, readmode) as f: + text = f.read() + if(CommonDefs.pyVer == 2): text = unicode(text, 'utf-8') + except Exception as e: + errormsg("Error reading the file specified in Text File input box."+ str(e)) + return + + if(text[0] == u'\ufeff'): + text = text[1:] + + if(flowInBox == True): + selElems = getSelectedElements(self) + selIds = [selElems[key].get('id') for key in selElems.keys()] + rectNodes = [r for r in self.document.xpath('//svg:rect', \ + namespaces=NSS) if r.get('id') in selIds] + if(len(rectNodes) == 0): + errormsg(_("No rectangle objects selected.")) + return + context.renderCharsInSelBoxes(text, rectNodes, margin, hAlignment, vAlignment, \ + False, expandDir, expandDist) + else: + context.renderCharsWithoutBox(text) + +runEffect(RenderStrokeFontText()) diff --git a/extensions/fablabchemnitz/stroke_font_creator/stroke_font_common.py b/extensions/fablabchemnitz/stroke_font_creator/stroke_font_common.py new file mode 100644 index 00000000..9cd251f9 --- /dev/null +++ b/extensions/fablabchemnitz/stroke_font_creator/stroke_font_common.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 + +''' +Defintion of Common functions and variables used by stroke font extensions + +Copyright (C) 2019 Shrinivas Kulkarni + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' + +import sys, os, fileinput, re, locale +from inkex import errormsg, addNS, NSS +from xml.dom.minidom import parse, Document +from math import ceil + +# TODO: Find inkscape version +try: + from lxml import etree + from inkex import Style, Boolean + from inkex.paths import Path, CubicSuperPath, Transform + from inkex import bezier + ver = 1.0 +except: + from inkex import etree + import simplestyle, cubicsuperpath, simplepath, simpletransform + from cubicsuperpath import CubicSuperPath + ver = 0.92 + try: + from simpletransform import computePointInNode + oldVersion = False + except: + oldVersion = True # older than 0.92 + +# sys path already includes the module folder +from stroke_font_manager import CharData, getFontNames, xAscent, \ + xDescent, xCapHeight, xXHeight, xSpaceROff, xFontId, xSize + +class CommonDefs: + inkVer = ver + pyVer = sys.version_info.major + + # inx filed that have the font list to be synchronized + inxFilesWithDynFont = ['render_stroke_font_text.inx', 'edit_stroke_font.inx'] + + vgScaleFact = 2. + lineT = .005 + + idAttribName = 'id' + hGuideIDPrefix = 'h_' + lvGuideIDPrefix = 'lv_' + rvGuideIDPrefix = 'rv_' + + fontOtherInfo = 'otherInfo' + + encoding = sys.stdin.encoding + if(encoding == 'cp0' or encoding is None): + encoding = locale.getpreferredencoding() + + +######### Function variants for 1.0 and 0.92 - Start ########## + +# Used only in 0.92 +def getPartsFromCubicSuper(csp): + parts = [] + for subpath in csp: + part = [] + prevBezPt = None + for i, bezierPt in enumerate(subpath): + if(prevBezPt != None): + seg = [prevBezPt[1], prevBezPt[2], bezierPt[0], bezierPt[1]] + part.append(seg) + prevBezPt = bezierPt + parts.append(part) + return parts + +def formatStyle(styleStr): + if(CommonDefs.inkVer == 1.0): + return str(Style(styleStr)) + else: + return simplestyle.formatStyle(styleStr) + +def getCubicSuperPath(d = None): + if(CommonDefs.inkVer == 1.0): + if(d == None): return CubicSuperPath([]) + return CubicSuperPath(Path(d).to_superpath()) + else: + if(d == None): return [] + return CubicSuperPath(simplepath.parsePath(d)) + +def getCubicLength(csp): + if(CommonDefs.inkVer == 1.0): + return bezier.csplength(csp)[1] + else: + parts = getPartsFromCubicSuper(cspath) + curveLen = 0 + for i, part in enumerate(parts): + for j, seg in enumerate(part): + curveLen += bezmisc.bezierlengthSimpson((seg[0], seg[1], seg[2], seg[3]), \ + tolerance = tolerance) + return curveLen + +def getCubicBoundingBox(csp): + if(CommonDefs.inkVer == 1.0): + bbox = csp.to_path().bounding_box() + return bbox.left, bbox.right, bbox.top, bbox.bottom + else: + return simpletransform.refinedBBox(csp) + +def formatSuperPath(csp): + if(CommonDefs.inkVer == 1.0): + return csp.__str__() + else: + return cubicsuperpath.formatPath(csp) + +def getParsedPath(d): + if(CommonDefs.inkVer == 1.0): + # Copied from Path.to_arrays for compatibility + return [[seg.letter, list(seg.args)] for seg in Path(d).to_absolute()] + else: + return simplepath.parsePath(d) + +def applyTransform(mat, csp): + if(CommonDefs.inkVer == 1.0): + csp.transform(mat) + else: + simpletransform.applyTransformToPath(mat, csp) + +def getTranslatedPath(d, posX, posY): + if(CommonDefs.inkVer == 1.0): + path = Path(d) + path.translate(posX, posY, inplace = True) + return path.to_superpath().__str__() + else: + path = simplepath.parsePath(d) + simplepath.translatePath(path, posX, posY) + return simplepath.formatPath(path) + +def getTransformMat(matAttr): + if(CommonDefs.inkVer == 1.0): + return Transform(matAttr) + else: + return simpletransform.parseTransform(matAttr) + +def getCurrentLayer(effect): + if(CommonDefs.inkVer == 1.0): + return effect.svg.get_current_layer() + else: + return effect.current_layer + +def getViewCenter(effect): + if(CommonDefs.inkVer == 1.0): + return effect.svg.namedview.center + else: + return effect.view_center + +def computePtInNode(vc, layer): + if(CommonDefs.inkVer == 1.0): + # ~ return (-Transform(layer.transform * mat)).apply_to_point(vc) + return (-layer.transform).apply_to_point(vc) + else: + if(oldVersion): + return list(vc) + else: + return computePointInNode(list(vc), layer) + +def getSelectedElements(effect): + if(CommonDefs.inkVer == 1.0): + return effect.svg.selected + else: + return effect.selected + +def getEtree(): + return etree + +def getAddFnTypes(effect): + if(CommonDefs.inkVer == 1.0): + addFn = effect.arg_parser.add_argument + typeFloat = float + typeInt = int + typeString = str + typeBool = Boolean + else: + addFn = effect.OptionParser.add_option + typeFloat = 'float' + typeInt = 'int' + typeString = 'string' + typeBool = 'inkbool' + + return addFn, typeFloat, typeInt, typeString, typeBool + +def runEffect(effect): + if(CommonDefs.inkVer == 1.0): effect.run() + else: effect.affect() + +######### Function variants for 1.0 and 0.92 - End ########## + +def getDecodedChars(chars): + if(CommonDefs.pyVer == 2): + return chars.decode(CommonDefs.encoding) + else: #if? + return chars + +def indentStr(cnt): + ostr = '' + for i in range(0, cnt): + ostr += ' ' + return ostr + +def getXMLItemsStr(sectMarkerLine, sectMarker, fontNames): + lSpaces = sectMarkerLine.find(sectMarker) + outStr = indentStr(lSpaces) + sectMarker + ' [start] -->\n' + for fName in fontNames: + outStr += indentStr(lSpaces + 4) + '' + fName + '\n' + outStr += indentStr(lSpaces) + sectMarker + ' [end] -->\n' + return outStr + +def syncFontList(extPath): + sectMarker = '