added stroke font creator
This commit is contained in:
parent
e423e1e7ea
commit
a6f397a8f8
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Custom Stroke Font - Edit Stroke Font</name>
|
||||
<id>fablabchemnitz.de.stroke_font_creator.edit_stroke_font</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="editStrokeFont" gui-text="Edit Stroke Font">
|
||||
<param name="fontName" type="optiongroup" appearance="combo" gui-text="Font:">
|
||||
<option value="Custom-Script">Custom-Script</option>
|
||||
<option value="Custom-Square Italic">Custom-Square Italic</option>
|
||||
<option value="Custom-Square Normal">Custom-Square Normal</option>
|
||||
<option value="Hershey-Astrology">Hershey-Astrology</option>
|
||||
<option value="Hershey-Cyrillic">Hershey-Cyrillic</option>
|
||||
<option value="Hershey-Gothic English">Hershey-Gothic English</option>
|
||||
<option value="Hershey-Gothic German">Hershey-Gothic German</option>
|
||||
<option value="Hershey-Gothic Italian">Hershey-Gothic Italian</option>
|
||||
<option value="Hershey-Greek 1-stroke">Hershey-Greek 1-stroke</option>
|
||||
<option value="Hershey-Greek medium">Hershey-Greek medium</option>
|
||||
<option value="Hershey-Japanese">Hershey-Japanese</option>
|
||||
<option value="Hershey-Markers">Hershey-Markers</option>
|
||||
<option value="Hershey-Math (lower)">Hershey-Math (lower)</option>
|
||||
<option value="Hershey-Math (upper)">Hershey-Math (upper)</option>
|
||||
<option value="Hershey-Meteorology">Hershey-Meteorology</option>
|
||||
<option value="Hershey-Music">Hershey-Music</option>
|
||||
<option value="Hershey-Sans 1-stroke">Hershey-Sans 1-stroke</option>
|
||||
<option value="Hershey-Sans bold">Hershey-Sans bold</option>
|
||||
<option value="Hershey-Script 1-stroke">Hershey-Script 1-stroke</option>
|
||||
<option value="Hershey-Script 1-stroke (alt)">Hershey-Script 1-stroke (alt)</option>
|
||||
<option value="Hershey-Script medium">Hershey-Script medium</option>
|
||||
<option value="Hershey-Serif bold">Hershey-Serif bold</option>
|
||||
<option value="Hershey-Serif bold italic">Hershey-Serif bold italic</option>
|
||||
<option value="Hershey-Serif medium">Hershey-Serif medium</option>
|
||||
<option value="Hershey-Serif medium italic">Hershey-Serif medium italic</option>
|
||||
<option value="Hershey-Symbolic">Hershey-Symbolic</option>
|
||||
</param>
|
||||
<param name="rowCnt" type="int" min="1" max="999999" gui-text="No of Rows:">5</param>
|
||||
<param name="fontSize" type="int" min="5" max="999999" gui-text="Font Size:">1000</param>
|
||||
<label>This extension overwrites the current document</label>
|
||||
</page>
|
||||
<page name="desc" gui-text="Help">
|
||||
<label xml:space="preserve">Inkscape extension for editing a stroke font.</label>
|
||||
</page>
|
||||
</param>
|
||||
<effect>
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Text"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">edit_stroke_font.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -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())
|
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Custom Stroke Font - Generate Font Data</name>
|
||||
<id>fablabchemnitz.de.stroke_font_creator.stroke_font_templ</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="genStrokeFontData" gui-text="Generate Font Data">
|
||||
<param name="fontName" type="string" gui-text="Font Name:" />
|
||||
<param name="rightOffsetType" type="optiongroup" appearance="combo" gui-text="Right Offset:">
|
||||
<option value="vgBbox">Vertical Guide / Bounding Box</option>
|
||||
<option value="lastNode">Main Segment Last Node</option>
|
||||
</param>
|
||||
<param name="crInfo" type="string" gui-text="Copyright Info:" />
|
||||
<param name="spaceWidth" type="float" min="0" max="999999" gui-text="Space Width (Enter 0 If Not Changed):">0</param>
|
||||
</page>
|
||||
<page name="desc" gui-text="Help">
|
||||
<label xml:space="preserve">Inkscape extension to generate the data of the stroke font glyphs from the current SVG.
|
||||
The data generated by this effect is used by the 'Render Text' extension.
|
||||
|
||||
Pre-requisite: This SVG should be created with the 'Create Font Design Template' extension
|
||||
The glyph bottom extent is derived from the reference horizontal guide(s)
|
||||
Based on the option selected, the Right offset of the glyph will be either:
|
||||
i) based on the right verical guide (if it exists) or bounding box or
|
||||
ii) derived from the x coordinate of the last node of the main segment
|
||||
in the glyph path (useful in case of script fonts)
|
||||
|
||||
Enter 0 in Space Width field if you don't want to change the existing value.</label>
|
||||
</page>
|
||||
</param>
|
||||
<effect needs-live-preview="false">
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Text"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">gen_stroke_font_data.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -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.')
|
20
extensions/fablabchemnitz/stroke_font_creator/meta.json
Normal file
20
extensions/fablabchemnitz/stroke_font_creator/meta.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "Custom Stroke Font - <various>",
|
||||
"id": "fablabchemnitz.de.stroke_font_creator.<various>",
|
||||
"path": "stroke_font_creator",
|
||||
"original_name": "<various>",
|
||||
"original_id": "khema.stroke.fnt.gen.<various>",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Custom Stroke Font - Render Text</name>
|
||||
<id>fablabchemnitz.de.stroke_font_creator.render_text</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="splash" gui-text="Render Text">
|
||||
<param name="text" type="string" gui-text="Text:" />
|
||||
<param name="filePath" type="string" gui-text="Text File:" />
|
||||
<param name="action" type="optiongroup" appearance="combo" gui-text="Action: ">
|
||||
<option value="renderText">Render the text</option>
|
||||
<option value="renderFile">Render text from file</option>
|
||||
<option value="renderTable">Render font glyph table</option>
|
||||
</param>
|
||||
<param name="fontName" type="optiongroup" appearance="combo" gui-text="Font:">
|
||||
<option value="Custom-Script">Custom-Script</option>
|
||||
<option value="Custom-Square Italic">Custom-Square Italic</option>
|
||||
<option value="Custom-Square Normal">Custom-Square Normal</option>
|
||||
<option value="Hershey-Astrology">Hershey-Astrology</option>
|
||||
<option value="Hershey-Cyrillic">Hershey-Cyrillic</option>
|
||||
<option value="Hershey-Gothic English">Hershey-Gothic English</option>
|
||||
<option value="Hershey-Gothic German">Hershey-Gothic German</option>
|
||||
<option value="Hershey-Gothic Italian">Hershey-Gothic Italian</option>
|
||||
<option value="Hershey-Greek 1-stroke">Hershey-Greek 1-stroke</option>
|
||||
<option value="Hershey-Greek medium">Hershey-Greek medium</option>
|
||||
<option value="Hershey-Japanese">Hershey-Japanese</option>
|
||||
<option value="Hershey-Markers">Hershey-Markers</option>
|
||||
<option value="Hershey-Math (lower)">Hershey-Math (lower)</option>
|
||||
<option value="Hershey-Math (upper)">Hershey-Math (upper)</option>
|
||||
<option value="Hershey-Meteorology">Hershey-Meteorology</option>
|
||||
<option value="Hershey-Music">Hershey-Music</option>
|
||||
<option value="Hershey-Sans 1-stroke">Hershey-Sans 1-stroke</option>
|
||||
<option value="Hershey-Sans bold">Hershey-Sans bold</option>
|
||||
<option value="Hershey-Script 1-stroke">Hershey-Script 1-stroke</option>
|
||||
<option value="Hershey-Script 1-stroke (alt)">Hershey-Script 1-stroke (alt)</option>
|
||||
<option value="Hershey-Script medium">Hershey-Script medium</option>
|
||||
<option value="Hershey-Serif bold">Hershey-Serif bold</option>
|
||||
<option value="Hershey-Serif bold italic">Hershey-Serif bold italic</option>
|
||||
<option value="Hershey-Serif medium">Hershey-Serif medium</option>
|
||||
<option value="Hershey-Serif medium italic">Hershey-Serif medium italic</option>
|
||||
<option value="Hershey-Symbolic">Hershey-Symbolic</option>
|
||||
<!-- ##! dynamically generated portion [end] -->
|
||||
</param>
|
||||
<param name="fontSize" type="float" min="1" max="999999" gui-text="Font Size:">20</param>
|
||||
<param name="charSpacing" type="float" min="0" max="100" gui-text="Char Spacing:">1</param>
|
||||
<param name="wordSpacing" type="float" min="0" max="100" gui-text="Word Spacing:">1</param>
|
||||
<param name="lineSpacing" type="float" min="0" max="100" gui-text="Line Spacing:">1.5</param>
|
||||
<param name="flowInBox" type="bool" gui-text="Flow Text in Selected Boxes:">true</param>
|
||||
<param name="margin" type="float" min="-999999" max="999999" gui-text="Margin:">5</param>
|
||||
<param name="hAlignment" type="optiongroup" appearance="combo" gui-text="Horizontal Alignment:">
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="justified">Justified</option>
|
||||
</param>
|
||||
<param name="vAlignment" type="optiongroup" appearance="combo" gui-text="Vertical Alignment:">
|
||||
<option value="none">None</option>
|
||||
<option value="top">Top</option>
|
||||
<option value="bottom">Bottom</option>
|
||||
<option value="center">Center</option>
|
||||
</param>
|
||||
<param name="expandDir" type="optiongroup" appearance="combo" gui-text="Create Extended Rectangles:">
|
||||
<option value="none">None</option>
|
||||
<option value="x">Horizontal Direction</option>
|
||||
<option value="y">Vertical Direction</option>
|
||||
</param>
|
||||
<param name="expandDist" type="float" min="0" max="100" gui-text="Extended Rectangle Offset:">1</param>
|
||||
</page>
|
||||
<page name="info" gui-text="Help">
|
||||
<label xml:space="preserve">This extension renders given text using the selected stroke font.
|
||||
|
||||
Action can be one of the following:
|
||||
- 'Render the text' renders the text from the Text input box
|
||||
(Use \n in the input string to start a new line of text)
|
||||
- 'Render text from file' renders the text from the file specified in the Text File input box
|
||||
- 'Render font glyph table' displays all the available glyphs of the given font
|
||||
along with the license information
|
||||
|
||||
If the 'Flow Text in Selected Boxes' option is selected, the text is fit into the selected rectangle objects with
|
||||
specified margin and justification. The rectangles are filled in their z order. If a single word cannot fit within the
|
||||
given width, it's broken into smaller components.
|
||||
|
||||
If there are errors, please ensure the font data files exist in the strokefontdata folder and
|
||||
the font list is synchronized.</label>
|
||||
</page>
|
||||
</param>
|
||||
<effect needs-live-preview="true" needs-document="true">
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Text"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">render_stroke_font_text.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -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())
|
@ -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) + '<item value="' + fName + '">' + fName + '</item>\n'
|
||||
outStr += indentStr(lSpaces) + sectMarker + ' [end] -->\n'
|
||||
return outStr
|
||||
|
||||
def syncFontList(extPath):
|
||||
sectMarker = '<!-- ##! dynamically generated portion'
|
||||
|
||||
sectMarkerLine = None
|
||||
xmlFilePaths = [extPath + "/" + f for f in CommonDefs.inxFilesWithDynFont]
|
||||
|
||||
try:
|
||||
fontNames = getFontNames(extPath)
|
||||
for xf in xmlFilePaths:
|
||||
for line in fileinput.input(xf, inplace = True):
|
||||
if sectMarker in line:
|
||||
if(sectMarkerLine != None):
|
||||
if(CommonDefs.pyVer == 3):
|
||||
# For some reasons python2 giving syntax error without eval
|
||||
eval("print(getXMLItemsStr(sectMarkerLine, sectMarker, fontNames), end = '')")
|
||||
else:
|
||||
print(getXMLItemsStr(sectMarkerLine, sectMarker, fontNames)),
|
||||
|
||||
sectMarkerLine = None
|
||||
else:
|
||||
sectMarkerLine = line
|
||||
else:
|
||||
if(sectMarkerLine == None):
|
||||
if(CommonDefs.pyVer == 3):
|
||||
eval("print(line, end = '')")
|
||||
else:
|
||||
print(line),
|
||||
|
||||
except Exception as e:
|
||||
errormsg('Error updating font list...\n' + str(e))
|
||||
|
||||
def addGridLine(layer, posX, posY, length, lType, style, attribs):
|
||||
line = etree.Element(addNS('path','svg'))
|
||||
d = 'M '+str(posX) + ' ' + str(posY) +' '+ lType +' '
|
||||
|
||||
if(lType == 'H'):
|
||||
d += str(posX + length)
|
||||
if(lType == 'V'):
|
||||
d += str(posY + length)
|
||||
|
||||
line.set('style', formatStyle(style))
|
||||
line.set('d', d)
|
||||
|
||||
for key in attribs:
|
||||
line.set(key, attribs[key])
|
||||
|
||||
layer.append(line)
|
||||
|
||||
def addText(layer, textStr, posX, posY, style):
|
||||
text = etree.Element(addNS('text','svg'))
|
||||
text.text = textStr
|
||||
|
||||
text.set('x', str(posX))
|
||||
text.set('y', str(posY))
|
||||
|
||||
text.set('style', formatStyle(style))
|
||||
|
||||
layer.append(text)
|
||||
|
||||
|
||||
def createTempl(callback, effect, extraInfo, rowCnt, glyphCnt, \
|
||||
vgScaleFact, createRvGuides, lineT, newCallBackLayerName = None):
|
||||
|
||||
hgStyle = {'stroke-width':str(lineT), 'opacity':'1', 'stroke':'#ff0066'}
|
||||
lvgStyle = {'stroke-width':str(lineT), 'opacity':'1', 'stroke':'#00aa88'}
|
||||
rvgStyle = {'stroke-width':str(lineT), 'opacity':'1', 'stroke':'#1b46ff'}
|
||||
|
||||
fontSize = extraInfo[xSize]
|
||||
spcY = fontSize * 3
|
||||
spcX = fontSize * 3
|
||||
|
||||
fontSize = extraInfo[xSize]
|
||||
vLineH = fontSize * vgScaleFact
|
||||
|
||||
colCnt = int(ceil(float(glyphCnt) / float(rowCnt)))
|
||||
|
||||
docW = (colCnt + 1) * spcX
|
||||
docH = (rowCnt + 1) * spcY
|
||||
|
||||
svg = effect.document.getroot()
|
||||
svg.set('width', str(docW))
|
||||
svg.set('height', str(docH))
|
||||
|
||||
#Remove viewbox
|
||||
if('viewBox' in svg.attrib):
|
||||
svg.attrib.pop('viewBox')
|
||||
|
||||
currLayers = svg.xpath('//svg:g', namespaces = NSS)
|
||||
for layer in currLayers:
|
||||
# Note: getparent()
|
||||
parentLayer = layer.getparent() if(CommonDefs.inkVer == 1.0) \
|
||||
else effect.getParentNode(layer)
|
||||
|
||||
if(parentLayer != None):
|
||||
parentLayer.remove(layer)
|
||||
|
||||
currExtraElems = svg.xpath('//svg:' + CommonDefs.fontOtherInfo, namespaces = NSS)
|
||||
for elem in currExtraElems:
|
||||
parentElem = elem.getparent() if(CommonDefs.inkVer == 1.0) \
|
||||
else effect.getParentNode(elem)
|
||||
parentElem.remove(elem)
|
||||
|
||||
extraInfoElem = etree.SubElement(svg, CommonDefs.fontOtherInfo)
|
||||
extraInfoElem.set(xAscent, str(extraInfo[xAscent]))
|
||||
extraInfoElem.set(xDescent, str(extraInfo[xDescent]))
|
||||
extraInfoElem.set(xCapHeight, str(extraInfo[xCapHeight]))
|
||||
extraInfoElem.set(xXHeight, str(extraInfo[xXHeight]))
|
||||
extraInfoElem.set(xSpaceROff, str(extraInfo[xSpaceROff]))
|
||||
extraInfoElem.set(xFontId, str(extraInfo[xFontId]))
|
||||
extraInfoElem.set(xSize, str(extraInfo[xSize]))
|
||||
|
||||
templLayer = etree.SubElement(svg, 'g')
|
||||
templLayer.set(addNS('label', 'inkscape'), 'Guides')
|
||||
templLayer.set(addNS('groupmode', 'inkscape'), 'layer')
|
||||
|
||||
if(newCallBackLayerName != None):
|
||||
callbackLayer = etree.SubElement(svg, 'g')
|
||||
callbackLayer.set(addNS('label', 'inkscape'), newCallBackLayerName)
|
||||
callbackLayer.set(addNS('groupmode', 'inkscape'), 'layer')
|
||||
else:
|
||||
callbackLayer = templLayer
|
||||
|
||||
editLayer = etree.SubElement(svg, 'g')
|
||||
editLayer.set(addNS('label', 'inkscape'), 'Glyphs')
|
||||
editLayer.set(addNS('groupmode', 'inkscape'), 'layer')
|
||||
editLayer.set('id', 'glyph')#TODO: How to make this dynamic?
|
||||
view = svg.namedview if CommonDefs.inkVer == 1.0 else effect.getNamedView()
|
||||
view.set(addNS('current-layer', 'inkscape'), editLayer.get('id'))
|
||||
|
||||
for row in range(0, rowCnt):
|
||||
|
||||
hAttribs = {CommonDefs.idAttribName : CommonDefs.hGuideIDPrefix + str(row)}
|
||||
addGridLine(templLayer, 0, \
|
||||
(row + 1) * spcY, docW, 'H', hgStyle, hAttribs)
|
||||
|
||||
for col in range(0, colCnt):
|
||||
glyphIdx = row * colCnt + col
|
||||
|
||||
if(glyphIdx >= glyphCnt):
|
||||
break
|
||||
|
||||
posX = (col + 1) * spcX
|
||||
posY = (row + 1) * spcY# + lineT / 2
|
||||
|
||||
#Caller can create whatever it wants at this position
|
||||
rOffset = callback(callbackLayer, editLayer, glyphIdx, posX, posY)
|
||||
if(rOffset == None):
|
||||
rOffset = fontSize
|
||||
|
||||
lvAttribs = {CommonDefs.idAttribName : CommonDefs.lvGuideIDPrefix + \
|
||||
str(row).zfill(4) + '_' + str(col).zfill(4)}
|
||||
|
||||
addGridLine(templLayer, \
|
||||
posX, posY + fontSize / 1.5, -vLineH, 'V', \
|
||||
lvgStyle, lvAttribs)
|
||||
|
||||
if(createRvGuides):
|
||||
rvAttribs = {CommonDefs.idAttribName : CommonDefs.rvGuideIDPrefix + \
|
||||
str(row).zfill(4) + '_' + str(col).zfill(4)}
|
||||
|
||||
addGridLine(templLayer, \
|
||||
posX + rOffset, posY + fontSize / 1.5, -vLineH, 'V', \
|
||||
rvgStyle, rvAttribs)
|
||||
|
||||
def getCharStyle(strokeWidth, naChar):
|
||||
#na character is a filled box
|
||||
naStyle = { 'stroke': '#000000', 'fill': '#000000', 'stroke-width': strokeWidth}
|
||||
charStyle = { 'stroke': '#000000', 'fill': 'none', 'stroke-width': strokeWidth,
|
||||
'stroke-linecap':'round', 'stroke-linejoin':'round'}
|
||||
|
||||
if(naChar):
|
||||
return naStyle
|
||||
else:
|
||||
return charStyle
|
||||
|
||||
class InkscapeCharData(CharData):
|
||||
def __init__(self, char, rOffset, pathStr, glyphName):
|
||||
self.pathStr = pathStr
|
||||
super(InkscapeCharData, self).__init__(char, rOffset, glyphName)
|
||||
|
||||
def getBBox(self):
|
||||
return getCubicBoundingBox(getCubicSuperPath(self.pathStr))
|
||||
|
||||
def scaleGlyph(self, scaleX, scaleY):
|
||||
self.rOffset *= scaleX
|
||||
cspath = getCubicSuperPath(self.pathStr)
|
||||
for subpath in cspath:
|
||||
for bezierPts in subpath:
|
||||
for i in range(0, len(bezierPts)):
|
||||
#No worries about origin...
|
||||
bezierPts[i] = [bezierPts[i][0] * scaleX, bezierPts[i][1] * scaleY]
|
||||
self.pathStr = formatSuperPath(cspath)
|
||||
self.bbox = getCubicBoundingBox(cspath)
|
||||
|
||||
class InkscapeCharDataFactory:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def getCharData(self, char, rOffset, pathStr, glyphName):
|
||||
return InkscapeCharData(char, rOffset, pathStr, glyphName)
|
@ -0,0 +1,711 @@
|
||||
#
|
||||
#
|
||||
# This module is used commonly by the Inkscape extension and the Blender add-on
|
||||
# that render the stroke font text
|
||||
#
|
||||
# Copyright (C) 2019 Shrinivas Kulkarni
|
||||
#
|
||||
# License: MIT
|
||||
#
|
||||
# Not yet pep8 compliant
|
||||
|
||||
import os, re, sys
|
||||
from xml.dom.minidom import parse, Document, getDOMImplementation
|
||||
|
||||
dataFileSubdir = 'strokefontdata'
|
||||
|
||||
##### XML Data Constants ########
|
||||
xDefs = 'defs'
|
||||
xFont = 'font'
|
||||
xFontId = 'id'
|
||||
xFontFace = 'font-face'
|
||||
xFontFamily = 'font-family'
|
||||
xCRInfo = 'metadata'
|
||||
xSize = 'units-per-em'
|
||||
xSpaceROff = 'horiz-adv-x'
|
||||
xChar = 'unicode'
|
||||
xROff = 'horiz-adv-x'
|
||||
xGlyph = 'glyph'
|
||||
xPath = 'd'
|
||||
xMissingGlyph = 'missing-glyph'
|
||||
xGlyphName = 'glyph-name'
|
||||
|
||||
xAscent='ascent'
|
||||
xDescent = 'descent'
|
||||
xCapHeight = 'cap-height'
|
||||
xXHeight = 'x-height'
|
||||
|
||||
|
||||
def getFontNames(parentPath):
|
||||
dataFileDirPath = parentPath + '/' + dataFileSubdir
|
||||
return sorted([fname[:-4] for fname in os.listdir(dataFileDirPath)
|
||||
if fname.endswith('.svg')], key=lambda s: s.lower())
|
||||
|
||||
def getDefaultExtraInfo(fontName, fontSize):
|
||||
info = {}
|
||||
info[xAscent] = 0.8 * fontSize
|
||||
info[xDescent] = -0.2 * fontSize
|
||||
info[xCapHeight] = 0.5 * fontSize
|
||||
info[xXHeight] = 0.3 * fontSize
|
||||
info[xSpaceROff] = 0.5 * fontSize
|
||||
info[xFontId] = ''.join(fontName.split(' '))
|
||||
info[xSize] = fontSize
|
||||
return info
|
||||
|
||||
|
||||
class CharData(object):
|
||||
def __init__(self, char, rOffset, glyphName = ''):
|
||||
self.char = char
|
||||
self.rOffset = rOffset
|
||||
self.bbox = self.getBBox() # Abstract
|
||||
self.glyphName = glyphName if glyphName != '' else char
|
||||
|
||||
|
||||
class FontData:
|
||||
def __init__(self, parentPath, fontName, fontSize, charDataFactory):
|
||||
dataFileDirPath = parentPath + '/' + dataFileSubdir
|
||||
self.dataFilePath = dataFileDirPath + '/' + fontName + '.svg'
|
||||
self.fontName = fontName
|
||||
self.glyphMap = {}
|
||||
self.fontSize = fontSize
|
||||
self.spaceWidth = fontSize / 2
|
||||
self.crInfo = ''
|
||||
fontDefs = None
|
||||
self.charDataFactory = charDataFactory
|
||||
|
||||
if(not os.path.isdir(dataFileDirPath)):
|
||||
os.makedirs(dataFileDirPath)
|
||||
|
||||
try:
|
||||
with open(self.dataFilePath, encoding="UTF-8") as xml:
|
||||
dataDoc = parse(xml)
|
||||
fontDefs = dataDoc.getElementsByTagName(xDefs)[0]
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if(fontDefs is not None):
|
||||
fontFaceElem = fontDefs.getElementsByTagName(xFontFace)[0]
|
||||
fontElem = fontDefs.getElementsByTagName(xFont)[0]
|
||||
|
||||
crElem = dataDoc.getElementsByTagName(xCRInfo)[0]
|
||||
if(len(crElem.childNodes) > 0):
|
||||
self.crInfo = crElem.childNodes[0].nodeValue
|
||||
|
||||
self.fontName = fontFaceElem.getAttribute(xFontFamily)
|
||||
oldFontSize = float(fontFaceElem.getAttribute(xSize))
|
||||
|
||||
info = {}
|
||||
|
||||
try:
|
||||
info[xSize] = oldFontSize
|
||||
info[xFontId] = fontElem.getAttribute(xFontId)
|
||||
info[xAscent] = float(fontFaceElem.getAttribute(xAscent))
|
||||
info[xDescent] = float(fontFaceElem.getAttribute(xDescent))
|
||||
info[xCapHeight] = float(fontFaceElem.getAttribute(xCapHeight))
|
||||
info[xXHeight] = float(fontFaceElem.getAttribute(xXHeight))
|
||||
info[xSpaceROff] = float(fontElem.getAttribute(xSpaceROff))
|
||||
except Exception as e:
|
||||
# ~ inkex.errormsg(str(e))
|
||||
info = getDefaultExtraInfo(self.fontName, oldFontSize)
|
||||
|
||||
glyphElems = fontDefs.getElementsByTagName(xGlyph)
|
||||
for e in glyphElems:
|
||||
char = e.getAttribute(xChar)
|
||||
rOffset = float(e.getAttribute(xROff))
|
||||
glyphName = e.getAttribute(xGlyphName)
|
||||
if(glyphName == 'space'):
|
||||
info[xSpaceROff] = rOffset
|
||||
else:
|
||||
pathStr = e.getAttribute(xPath)
|
||||
if(pathStr != None and pathStr.strip() != ''):
|
||||
charData = charDataFactory.getCharData(char, rOffset, pathStr, glyphName)
|
||||
|
||||
scaleFact = fontSize / oldFontSize
|
||||
charData.scaleGlyph(scaleFact, -scaleFact)
|
||||
|
||||
self.glyphMap[char] = charData
|
||||
|
||||
self.extraInfo = {}
|
||||
for key in info:
|
||||
if(isinstance(info[key], float) or isinstance(info[key], int)):
|
||||
self.extraInfo[key] = fontSize * info[key] / oldFontSize
|
||||
else:
|
||||
self.extraInfo[key] = info[key]
|
||||
|
||||
self.spaceWidth = self.extraInfo[xSpaceROff]
|
||||
else:
|
||||
self.extraInfo = getDefaultExtraInfo(self.fontName, self.fontSize)
|
||||
|
||||
def updateGlyph(self, char, rOffset, pathStr, glyphName):
|
||||
charData = self.charDataFactory.getCharData(char, rOffset, pathStr, glyphName)
|
||||
self.glyphMap[char] = charData
|
||||
|
||||
def setCRInfo(self, crInfo):
|
||||
if(crInfo is not None and crInfo != ''):
|
||||
self.crInfo = crInfo
|
||||
|
||||
def setExtraInfo(self, extraInfo):
|
||||
self.extraInfo = extraInfo
|
||||
|
||||
def hasGlyphs(self):
|
||||
return len(self.glyphMap) > 0
|
||||
|
||||
# invertY = True because glyph was inverted in FontData constructor
|
||||
def updateFontXML(self, invertY = True):
|
||||
imp = getDOMImplementation()
|
||||
doctype = imp.createDocumentType('svg', "-//W3C//DTD SVG 1.1//EN", \
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd")
|
||||
doc = imp.createDocument(None, 'svg', doctype)
|
||||
docElem = doc.documentElement
|
||||
docElem.setAttribute("xmlns", "http://www.w3.org/2000/svg")
|
||||
docElem.setAttributeNS("xmls", "xmlns:xlink", "http://www.w3.org/1999/xlink")
|
||||