added stroke font creator
@ -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
@ -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")
|
||||
docElem.setAttribute("version", "1.1")
|
||||
|
||||
fontDefs = doc.createElement(xDefs)
|
||||
docElem.appendChild(fontDefs)
|
||||
|
||||
info = self.extraInfo
|
||||
spaceWidthStr = str(info[xSpaceROff])
|
||||
|
||||
fontElem = doc.createElement(xFont)
|
||||
fontElem.setAttribute(xSpaceROff, spaceWidthStr)
|
||||
fontElem.setAttribute(xFontId, self.fontName)
|
||||
fontDefs.appendChild(fontElem)
|
||||
|
||||
fontFaceElem = doc.createElement(xFontFace)
|
||||
fontFaceElem.setAttribute(xFontFamily, self.fontName)
|
||||
fontFaceElem.setAttribute(xSize, str(self.fontSize))
|
||||
fontFaceElem.setAttribute(xAscent, str(info[xAscent]))
|
||||
fontFaceElem.setAttribute(xDescent, str(info[xDescent]))
|
||||
fontFaceElem.setAttribute(xCapHeight, str(info[xCapHeight]))
|
||||
fontFaceElem.setAttribute(xXHeight, str(info[xXHeight]))
|
||||
|
||||
fontElem.appendChild(fontFaceElem)
|
||||
|
||||
crElem = doc.createElement(xCRInfo)
|
||||
docElem.appendChild(crElem)
|
||||
|
||||
if (self.crInfo != ''):
|
||||
crElem.appendChild(doc.createTextNode(self.crInfo))
|
||||
|
||||
missingGlyphElem = doc.createElement(xMissingGlyph)
|
||||
missingGlyphElem.setAttribute(xROff, spaceWidthStr)
|
||||
fontElem.appendChild(missingGlyphElem)
|
||||
|
||||
glyphElem = doc.createElement(xGlyph)
|
||||
glyphElem.setAttribute(xChar, ' ')
|
||||
glyphElem.setAttribute(xGlyphName, 'space')
|
||||
glyphElem.setAttribute(xROff, spaceWidthStr)
|
||||
fontElem.appendChild(glyphElem)
|
||||
|
||||
for char in self.glyphMap:
|
||||
charData = self.glyphMap[char]
|
||||
if(invertY): charData.scaleGlyph(1, -1)
|
||||
glyphElem = doc.createElement(xGlyph)
|
||||
|
||||
glyphElem.setAttribute(xChar, char)
|
||||
glyphElem.setAttribute(xGlyphName, charData.glyphName)
|
||||
glyphElem.setAttribute(xPath, charData.pathStr)
|
||||
glyphElem.setAttribute(xROff, str(charData.rOffset))
|
||||
|
||||
fontElem.appendChild(glyphElem)
|
||||
|
||||
with open(self.dataFilePath, "w", encoding="UTF-8") as f:
|
||||
xmlStr = doc.toxml(encoding="UTF-8")
|
||||
if(sys.version_info.major == 3): xmlStr = xmlStr.decode("UTF-8")
|
||||
f.write(xmlStr)
|
||||
|
||||
|
||||
class DrawContext:
|
||||
|
||||
#bottomToTop flag indicates y increases from bottom towards top (e.g. in Blender)
|
||||
def __init__(self, parentPath, fontName, fontSize, charSpacing, wordSpacing,
|
||||
lineSpacing, charDataFactory, renderer, bottomToTop = False):
|
||||
self.charSpacing = charSpacing
|
||||
self.lineSpacing = lineSpacing
|
||||
self.renderer = renderer
|
||||
self.bottomToTop = bottomToTop
|
||||
self.yCoeff = -1 if(bottomToTop) else 1
|
||||
|
||||
self.strokeFontData = FontData(parentPath, fontName, fontSize, charDataFactory)
|
||||
|
||||
self.spaceWidth = self.strokeFontData.spaceWidth * wordSpacing
|
||||
self.lineHeight = fontSize * lineSpacing
|
||||
|
||||
def fontHasGlyphs(self):
|
||||
return self.strokeFontData.hasGlyphs()
|
||||
|
||||
def getCharData(self, char):
|
||||
cd = self.strokeFontData.glyphMap.get(char)
|
||||
|
||||
if(cd is None):
|
||||
naMargin = self.strokeFontData.fontSize * .1
|
||||
naSize = self.strokeFontData.fontSize * .5
|
||||
naOffset = (naSize + naMargin * 2)
|
||||
|
||||
naPathStr = 'M ' + str(naMargin) + ',' + str(0) + ' h ' + \
|
||||
str(naSize) + ' v ' + str(-1 * self.yCoeff * naSize) + \
|
||||
' h ' + str(-1 * naSize) + ' Z'
|
||||
|
||||
cd = self.strokeFontData.charDataFactory.getCharData(char, \
|
||||
naOffset, naPathStr, 'na')
|
||||
|
||||
cd.bbox = [naMargin, naMargin + naSize, -1 * self.yCoeff * naSize, 0]
|
||||
|
||||
return cd
|
||||
|
||||
def getLineTopY(self, spaceWordData):
|
||||
topY = 0
|
||||
|
||||
wordData = [c for w in spaceWordData if (w[1] is not None) for c in w[1]]
|
||||
|
||||
for i, c in enumerate(wordData):
|
||||
# Reverse the comparison if y increases from bottom to top
|
||||
if (i == 0 or (self.yCoeff * c.bbox[2] < self.yCoeff * topY)):
|
||||
topY = c.bbox[2]
|
||||
|
||||
return topY
|
||||
|
||||
def getLineBottomY(self, spaceWordData):
|
||||
bottomY = 0
|
||||
|
||||
wordData = [c for w in spaceWordData if (w[1] is not None) for c in w[1]]
|
||||
|
||||
for i, c in enumerate(wordData):
|
||||
# Reverse the comparison if y increases from bottom to top
|
||||
if (i == 0 or self.yCoeff * c.bbox[3] > self.yCoeff * bottomY):
|
||||
bottomY = c.bbox[3]
|
||||
|
||||
return bottomY
|
||||
|
||||
# Calculate the width distribution among spaces for justified alignment
|
||||
# The apportioned width is proportional to word length and width of
|
||||
# preceding spaces
|
||||
def getDistWidths(self, spaceWordData, x, xRight):
|
||||
|
||||
totalLineLen = self.getWordLineLen(spaceWordData)
|
||||
|
||||
# Extra space to be distributed
|
||||
extra = xRight - x - totalLineLen
|
||||
|
||||
# Total length to be considered
|
||||
llen = totalLineLen
|
||||
start = 0
|
||||
eLens = []
|
||||
|
||||
if(spaceWordData[0][0] == 0):
|
||||
# Subtract the first word length as it's not considered in calculation
|
||||
# (spaces start after it)
|
||||
llen -= self.getWordLen(spaceWordData[0][1])
|
||||
start = 1
|
||||
eLens.append(0)
|
||||
|
||||
for i in range(start, len(spaceWordData)):
|
||||
sw = spaceWordData[i]
|
||||
eL = sw[0] * self.spaceWidth + self.getWordLen(sw[1])
|
||||
eLens.append(eL * extra / llen)
|
||||
|
||||
return eLens
|
||||
|
||||
# In case a single word length is greater than rect width,
|
||||
# need to split it
|
||||
def splitWord(self, wordData, rectw):
|
||||
wordComps = []
|
||||
comp1 = wordData
|
||||
|
||||
while(len(comp1) > 0 and self.getWordLen(comp1) > rectw):
|
||||
comp2 = []
|
||||
while(len(comp1) > 0 and self.getWordLen(comp1) > rectw):
|
||||
comp2.insert(0, comp1.pop())
|
||||
|
||||
# Rectangle can't even fit a single letter
|
||||
if(len(comp1) == 0):
|
||||
break
|
||||
|
||||
wordComps.append(comp1)
|
||||
comp1 = comp2
|
||||
|
||||
wordComps.append(comp1)
|
||||
return wordComps
|
||||
|
||||
# Word boundary is bbox minX of first letter upto bbox maxX of last
|
||||
# with charSpace * rOffsets dist between the chars in between
|
||||
def drawWordWithLenCalc(self, wordData, render=False, x=0, y=0):
|
||||
if(wordData is None or len(wordData) == 0):
|
||||
return 0
|
||||
|
||||
nextX = x
|
||||
|
||||
for i, charData in enumerate(wordData):
|
||||
# Always start from bbox minX of the first letter
|
||||
if(i == 0):
|
||||
nextX -= charData.bbox[0]
|
||||
|
||||
if(render):
|
||||
naChar = self.strokeFontData.glyphMap.get(charData.char) is None
|
||||
self.renderer.renderChar(charData, nextX, y, naChar)
|
||||
|
||||
xmax = charData.bbox[1]
|
||||
|
||||
# Possible that a char other than the last one ends after it
|
||||
# Extreme case: charSpacing = 0
|
||||
if(i == 0 or nextX + xmax > maxLen):
|
||||
maxLen = nextX + xmax
|
||||
|
||||
nextX += charData.rOffset * self.charSpacing
|
||||
|
||||
# Calculate getWordLen separately because of last and first char
|
||||
# exceptions
|
||||
return maxLen
|
||||
|
||||
def getWordLen(self, wordData):
|
||||
return self.drawWordWithLenCalc(wordData)
|
||||
|
||||
def drawWord(self, x, y, wordData):
|
||||
return self.drawWordWithLenCalc(wordData, True, x, y)
|
||||
|
||||
# Length of line of words, includes trailing spaces
|
||||
def getWordLineLen(self, spaceWordData):
|
||||
if(spaceWordData is None or len(spaceWordData) == 0):
|
||||
return 0
|
||||
|
||||
wlLen = 0
|
||||
cnt = len(spaceWordData)
|
||||
|
||||
# If last word is None, there are trailing spaces, consider their
|
||||
# length
|
||||
if(spaceWordData[-1][1] is None):
|
||||
cnt -= 1
|
||||
wlLen = spaceWordData[-1][0] * self.spaceWidth
|
||||
|
||||
# spaceWordData is word and the length of its preceding spaces
|
||||
for i in range(0, cnt):
|
||||
sw = spaceWordData[i]
|
||||
wlLen += sw[0] * self.spaceWidth
|
||||
wlLen += self.getWordLen(sw[1])
|
||||
|
||||
return wlLen
|
||||
|
||||
def drawWordLine(self, spaceWordData, x, y, xRight, alignment):
|
||||
lineLen = self.getWordLineLen(spaceWordData)
|
||||
|
||||
if(alignment == 'right'):
|
||||
x = xRight - lineLen
|
||||
elif(alignment == 'center'):
|
||||
x += (xRight - x - lineLen) / 2
|
||||
elif(alignment == 'justified'):
|
||||
eLens = self.getDistWidths(spaceWordData, x, xRight)
|
||||
|
||||
nextX = x
|
||||
for i, sw in enumerate(spaceWordData):
|
||||
|
||||
nextX += sw[0] * self.spaceWidth
|
||||
|
||||
if(alignment == 'justified'):
|
||||
nextX += eLens[i]
|
||||
|
||||
nextX = self.drawWord(nextX, y, sw[1])
|
||||
|
||||
return nextX
|
||||
|
||||
# the chars must end with \n
|
||||
def renderCharsInBox(self, chars, xLeft, yTop, xRight, yBottom, hAlignment, vAlignment):
|
||||
|
||||
spaceWordData = []
|
||||
wordData = []
|
||||
procCharsIdx = 0
|
||||
x = xLeft
|
||||
y = yTop
|
||||
yTextBottom = yTop
|
||||
|
||||
for i, char in enumerate(chars):
|
||||
if(char != ' ' and char != '\n'):
|
||||
charData = self.getCharData(char)
|
||||
wordData.append(charData)
|
||||
continue
|
||||
|
||||
# At this point, wordData will have accumulated chars before this space/newline
|
||||
# Last element of spaceWordData will have spacewidths to be inserted before
|
||||
# this word data
|
||||
if(len(wordData) > 0):
|
||||
wLen = self.getWordLen(wordData)
|
||||
|
||||
# Includes trailing spaces
|
||||
prevLineLen = self.getWordLineLen(spaceWordData)
|
||||
|
||||
if(x + prevLineLen + wLen > xRight):
|
||||
|
||||
trailingSpaces = 0
|
||||
if(len(spaceWordData) > 0 and spaceWordData[-1][1] is None):
|
||||
trailingSpaces = spaceWordData.pop()[0]
|
||||
|
||||
# Create an array in case this single word is bigger than rect width,
|
||||
# so that all chunks can be drawn here itself
|
||||
# Most likely this array will contain only the line
|
||||
# accumulate before this word
|
||||
spaceWordDataArr = []
|
||||
|
||||
# Exhaust the line with the previous words first
|
||||
if(len(spaceWordData) > 0):
|
||||
spaceWordDataArr.append(spaceWordData)
|
||||
|
||||
# This single word is longer than the rect width can fit
|
||||
if(x + wLen > xRight):
|
||||
wordComps = self.splitWord(wordData, (xRight - x))
|
||||
|
||||
# Not even a single letter fits, so return
|
||||
if(len(wordComps) == 1):
|
||||
return yTextBottom, chars[procCharsIdx:]
|
||||
|
||||
# Add all the chunks, each on a new line
|
||||
spaceWordDataArr += [[[0, wordComps[k]]]
|
||||
for k in range(0, len(wordComps) - 1)]
|
||||
|
||||
wordData = wordComps[-1]
|
||||
|
||||
# Draw as many lines as there are elements in the
|
||||
# spaceWordDataArr
|
||||
while(len(spaceWordDataArr) > 0):
|
||||
spaceWordData = spaceWordDataArr.pop(0)
|
||||
if(len(spaceWordData) > 0):
|
||||
|
||||
#TODO repetition(1)
|
||||
# If the first line, align its top edge along the rect top
|
||||
if(y == yTop):
|
||||
y -= self.getLineTopY(spaceWordData)
|
||||
|
||||
lineBottom = (y + self.getLineBottomY(spaceWordData))
|
||||
|
||||
if(self.yCoeff * lineBottom > self.yCoeff * yBottom):
|
||||
return yTextBottom, chars[procCharsIdx:]
|
||||
|
||||
self.drawWordLine(spaceWordData, x, y, xRight, hAlignment)
|
||||
|
||||
yTextBottom = lineBottom
|
||||
|
||||
# Shift marker for processed chars
|
||||
procCharsIdx = i - \
|
||||
sum(len(wd[0][1]) for wd in spaceWordDataArr) - len(wordData)
|
||||
|
||||
# This has to be repeated for 2 diff conditions,
|
||||
# Complications not worth it.. so commenting out
|
||||
# (Just ignore long sequence of trailing spaces for now)
|
||||
# ~ if(x + trailingSpaces * self.spaceWidth > xRight \
|
||||
# ~ and len(spaceWordDataArr) > 0):
|
||||
# ~ y += self.lineHeight
|
||||
|
||||
trailingSpaces = 0
|
||||
y += self.yCoeff * self.lineHeight
|
||||
|
||||
spaceWordData = [[0, wordData]]
|
||||
else:
|
||||
if(len(spaceWordData) == 0):
|
||||
spaceWordData = [[0, wordData]]
|
||||
else:
|
||||
spaceWordData[-1][1] = wordData
|
||||
|
||||
wordData = []
|
||||
|
||||
if(char == ' '):
|
||||
if(len(spaceWordData) == 0 or spaceWordData[-1][1] is not None):
|
||||
spaceWordData.append([1, None])
|
||||
else:
|
||||
spaceWordData[-1][0] += 1
|
||||
|
||||
elif(char == '\n'):
|
||||
# Don't consider trailing spaces if hard new line
|
||||
if(len(spaceWordData) > 0 and spaceWordData[-1][1] is None):
|
||||
spaceWordData.pop()
|
||||
|
||||
if(len(spaceWordData) > 0):
|
||||
#TODO repetition(2)
|
||||
if(y == yTop):
|
||||
y -= self.getLineTopY(spaceWordData)
|
||||
|
||||
lineBottom = (y + self.getLineBottomY(spaceWordData))
|
||||
|
||||
elif(vAlignment == 'none'):
|
||||
lineBottom = (y + self.yCoeff * self.lineHeight)
|
||||
else:
|
||||
lineBottom = yTextBottom #Get from the previous text line
|
||||
|
||||
if(self.yCoeff * lineBottom > self.yCoeff * yBottom):
|
||||
return yTextBottom, chars[procCharsIdx:]
|
||||
|
||||
if(len(spaceWordData) > 0):
|
||||
self.drawWordLine(spaceWordData, x, y, xRight, hAlignment
|
||||
if hAlignment != 'justified' else 'left')
|
||||
yTextBottom = lineBottom
|
||||
|
||||
if(vAlignment == 'none' or len(spaceWordData) > 0):
|
||||
y += self.yCoeff * self.lineHeight
|
||||
|
||||
procCharsIdx = i
|
||||
|
||||
spaceWordData = []
|
||||
|
||||
return yTextBottom, None
|
||||
|
||||
def renderCharsWithoutBox(self, chars):
|
||||
|
||||
if(chars is None or len(chars) == 0):
|
||||
return
|
||||
|
||||
x, y = self.renderer.getDefaultStartLocation()
|
||||
xRight = sys.float_info.max
|
||||
|
||||
regex = re.compile('([ ]*)([^ ]+)')
|
||||
lines = chars.split('\n')
|
||||
|
||||
wmax = 0
|
||||
hmax = 0
|
||||
|
||||
self.renderer.beforeRender()
|
||||
|
||||
for line in lines:
|
||||
res = regex.findall(line)
|
||||
spaceWordData = []
|
||||
for r in res:
|
||||
wordData = [self.getCharData(cd) for cd in r[1]]
|
||||
spaceWordData.append([len(r[0]), wordData])
|
||||
|
||||
xr = self.drawWordLine(
|
||||
spaceWordData, x, y, xRight, alignment='left')
|
||||
|
||||
if(xr > wmax):
|
||||
wmax = xr
|
||||
|
||||
y += self.yCoeff * self.lineHeight
|
||||
|
||||
self.renderer.centerInView(wmax / 2, y / 2)
|
||||
|
||||
#Remove newline chars if alignment is not none
|
||||
def preprocess(self, chars, vAlignment, isFirstLine):
|
||||
newLine = False
|
||||
|
||||
while(chars.startswith('\n')):
|
||||
newLine = True
|
||||
if(vAlignment != 'none'):
|
||||
chars = chars[1:]
|
||||
else:
|
||||
break
|
||||
|
||||
# Retain leading spaces in case of newline ending and the very
|
||||
# first line of the text
|
||||
if(not newLine and not isFirstLine):
|
||||
chars = chars.strip()
|
||||
|
||||
#required for the processing function
|
||||
if(not chars.endswith('\n')):
|
||||
chars += '\n'
|
||||
|
||||
return chars
|
||||
|
||||
def renderCharsInSelBoxes(self, chars, rectangles, margin, hAlignment, vAlignment, \
|
||||
addPlane = False, expandDir = None, expandDist = None):
|
||||
|
||||
self.renderer.beforeRender()
|
||||
i = 0
|
||||
|
||||
while(chars != None):
|
||||
if(i < len(rectangles)):
|
||||
box = rectangles[i]
|
||||
elif(expandDir != None and expandDist != None):
|
||||
x1, y1, x2, y2 = self.renderer.getBoxLeftTopRightBottom(rectangles[-1])
|
||||
w = abs(x2 - x1)
|
||||
h = abs(y2 - y1)
|
||||
|
||||
if(expandDir == 'x'):
|
||||
x1 += w + expandDist
|
||||
x2 += w + expandDist
|
||||
elif(expandDir == 'y'):
|
||||
y1 += self.yCoeff * (h + expandDist)
|
||||
y2 += self.yCoeff * (h + expandDist)
|
||||
#TODO: Neat hadling for 2d rendering
|
||||
elif(expandDir == 'z'):
|
||||
self.renderer.z += expandDist
|
||||
box = self.renderer.getBoxFromCoords(x1, y1, x2, y2)
|
||||
rectangles.append(box)
|
||||
else:
|
||||
break
|
||||
|
||||
if(len(chars) == 0):
|
||||
return
|
||||
|
||||
self.renderer.newBoxToBeRendered(box, addPlane)
|
||||
|
||||
x1, y1, x2, y2 = self.renderer.getBoxLeftTopRightBottom(box)
|
||||
|
||||
xLeft = x1 + margin
|
||||
yTop = y1 + self.yCoeff * margin
|
||||
|
||||
xRight = x2 - margin
|
||||
yBottom = y2 - self.yCoeff * margin
|
||||
|
||||
chars = self.preprocess(chars, vAlignment, (i == 0))
|
||||
lenCharsBeforeProc = len(chars)
|
||||
|
||||
yTextBottom, chars = self.renderCharsInBox(chars, xLeft, yTop, \
|
||||
xRight, yBottom, hAlignment, vAlignment)
|
||||
|
||||
if(vAlignment == 'center'):
|
||||
moveBy = (yBottom - yTextBottom) / 2
|
||||
self.renderer.moveBoxInYDir(moveBy)
|
||||
elif(vAlignment == 'bottom'):
|
||||
moveBy = yBottom - yTextBottom
|
||||
self.renderer.moveBoxInYDir(moveBy)
|
||||
|
||||
if(chars is None or \
|
||||
(lenCharsBeforeProc == len(chars) and (len(rectangles)-1) == i)):
|
||||
return
|
||||
|
||||
i += 1
|
||||
|
||||
def renderGlyphTable(self):
|
||||
self.renderer.beforeRender()
|
||||
|
||||
xStart, y = self.renderer.getDefaultStartLocation()
|
||||
x = xStart
|
||||
|
||||
chars = [c for c in self.strokeFontData.glyphMap.keys()]
|
||||
chars = sorted(chars)
|
||||
|
||||
text = "Font: " + self.strokeFontData.fontName
|
||||
self.renderer.renderPlainText(
|
||||
text, self.strokeFontData.fontSize, x, y, 'Font Name')
|
||||
|
||||
y += self.yCoeff * self.strokeFontData.fontSize
|
||||
|
||||
crInfoTxt = self.strokeFontData.crInfo
|
||||
|
||||
maxStrSize = 100
|
||||
|
||||
infoLines = crInfoTxt.split('\n')
|
||||
for line in infoLines:
|
||||
while(line != ""):
|
||||
crInfo = line[:maxStrSize]
|
||||
line = line[maxStrSize:]
|
||||
if(crInfo[-1].isalpha() and len(crInfoTxt) > 0 and crInfoTxt[0].isalpha()):
|
||||
crInfo += '-'
|
||||
self.renderer.renderPlainText(
|
||||
crInfo, self.strokeFontData.fontSize / 2, x, y, 'CR Info')
|
||||
|
||||
y += self.yCoeff * self.strokeFontData.fontSize
|
||||
y += self.yCoeff * .5 * self.strokeFontData.fontSize
|
||||
|
||||
hCnt = 10
|
||||
letterSpace = self.strokeFontData.fontSize * 2
|
||||
|
||||
for i, char in enumerate(chars):
|
||||
if(i % hCnt == 0):
|
||||
x = xStart
|
||||
if(i > 0):
|
||||
y += self.yCoeff * self.lineHeight
|
||||
self.renderer.renderPlainText(char, self.strokeFontData.fontSize / 2, x, y, char)
|
||||
x += letterSpace / 3
|
||||
self.drawWord(x, y, [self.getCharData(char)])
|
||||
x += letterSpace
|
||||
|
||||
width = letterSpace * hCnt / 2
|
||||
height = y / 2
|
||||
self.renderer.centerInView(width, height)
|
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Custom Stroke Font - Create Font Design Template</name>
|
||||
<id>stroke_font_creator._stroke_font_templ</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="createStrokeFontTempl" gui-text="Create Font Design Template">
|
||||
<param name="rowCnt" type="int" min="1" max="999999" gui-text="No of Rows:">5</param>
|
||||
<param name="glyphCnt" type="int" min="1" max="999999" gui-text="Glyph Count:">175</param>
|
||||
<param name="fontSize" type="int" min="5" max="999999" gui-text="Font Size:">1000</param>
|
||||
<param name="spaceWidth" type="int" min="1" max="999999" gui-text="Space Width:">500</param>
|
||||
<param name="rvGuides" type="bool" gui-text="Right Vertical Guides">false</param>
|
||||
<spacer />
|
||||
<separator />
|
||||
<spacer />
|
||||
<param name="createGlyphs" type="bool" gui-text="Create Source Glyphs">true</param>
|
||||
<param name="srcFontFamily" type="string" gui-text="Source Font Family:" />
|
||||
<param name="fontType" type="optiongroup" appearance="combo" gui-text="Source Font Style:">
|
||||
<option value="normal">Regular</option>
|
||||
<option value="italic">Italic</option>
|
||||
<option value="bold">Bold</option>
|
||||
<option value="bolditalic">Bold Italic</option>
|
||||
</param>
|
||||
<param name="strokeOpacity" type="float" gui-text="Source Glyph Stroke Opacity">1</param>
|
||||
<param name="fillOpacity" type="float" gui-text="Source Glyph Fill Opacity">0.1</param>
|
||||
<param name="startGlyph" type="string" max_length="4" gui-text="First Glyph:">0</param>
|
||||
<spacer />
|
||||
<label>This extension overwrites the current document</label>
|
||||
</page>
|
||||
<page name="desc" gui-text="Help">
|
||||
<label xml:space="preserve">Inkscape extension to generate template for designing / tracing custom stroke font.
|
||||
|
||||
Check the 'Generate Source Glyphs' option to create characters of the given size, stroke and fill opacity from the source font family; these can be used for tracing the stroke font glyphs.
|
||||
You can optionally enter the first character or its unicode value in the 'First Glyph' text box. To input the unicode value, enter the 4 digit hex unicode in this field (e.g. 00C0 for À). Or you can directly enter just À. This character will be considered for rendering the font family glyphs in the template. If there is no entry in 'First Glyph' and if 'Generate Source Glyphs' is checked, the font family glyphs start from glyph corresponding to the character A.
|
||||
Checking the 'Right Vertical Guides' options, creates an extra vertical guide at the right extreme of each glyph for manual setting of right offset.
|
||||
In Space Width field enter the width of blank space between rendered words. This is generally 1/3 to 1/2 times the Font Size.</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">stroke_font_templ.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
'''
|
||||
Inkscape extension to generate template for designing / tracing custom stroke fonts
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
from inkex import Effect
|
||||
import sys, os
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
from stroke_font_common import CommonDefs, getDecodedChars, createTempl, addText
|
||||
from stroke_font_common import getAddFnTypes, runEffect
|
||||
from stroke_font_manager import xSpaceROff, getDefaultExtraInfo
|
||||
|
||||
class CustStrokeFontTempl(Effect):
|
||||
|
||||
def __init__(self):
|
||||
Effect.__init__(self)
|
||||
|
||||
addFn, typeFloat, typeInt, typeString, typeBool = getAddFnTypes(self)
|
||||
|
||||
addFn('--rowCnt', action = 'store', type = typeInt, dest = 'rowCnt', default = '5', \
|
||||
help = 'Number of rows (horizontal guides) in the template')
|
||||
|
||||
addFn("--createGlyphs", action="store", type=typeBool, dest="createGlyphs", \
|
||||
default=True, help = 'Render glyphs of the source font family for tracing ')
|
||||
|
||||
addFn('--srcFontFamily', action = 'store', type = typeString, dest = 'srcFontFamily', \
|
||||
help = 'Exact name of the source font family')
|
||||
|
||||
addFn('--fontSize', action = 'store', type = typeInt, dest = 'fontSize', default = '100', \
|
||||
help = 'Size of the source glyphs to be rendered')
|
||||
|
||||
addFn('--spaceWidth', action = 'store', \
|
||||
type = typeInt, dest = 'spaceWidth', default = '50', \
|
||||
help = 'Width of the space character (generally 1/3 to 1/2 times the font size')
|
||||
|
||||
addFn('--fontType', action = 'store', type = typeString, dest = 'fontType', \
|
||||
default = 'normal', help = 'Font Style')
|
||||
|
||||
addFn('--startGlyph', action = 'store', type = typeString, dest = 'startGlyph', \
|
||||
default = '0', help = 'Starting glyph to be rendered')
|
||||
|
||||
addFn('--glyphCnt', action = 'store', type = typeInt, dest = 'glyphCnt', \
|
||||
default = '75', help = 'Number of template glyphs')
|
||||
|
||||
addFn("--rvGuides", action = "store", type = typeBool, dest = "rvGuides", \
|
||||
default = False, help = 'Render vertical guide at the right of each glyph')
|
||||
|
||||
addFn("--fillOpacity", action = "store", type = typeFloat, dest = "fillOpacity", \
|
||||
default = False, help = 'Fill opacity of source glyph')
|
||||
|
||||
addFn("--strokeOpacity", action = "store", type = typeFloat, dest = "strokeOpacity", \
|
||||
default = False, help = 'Stroke opacity of source glyph')
|
||||
|
||||
addFn("--tab", action = "store", type = typeString, dest = "tab", \
|
||||
default = "sampling", help="Tab")
|
||||
|
||||
def addElem(self, sourceGlyphLayer, editLayer, glyphIdx, posX, posY):
|
||||
if(self.createTTGlyphs):
|
||||
if(CommonDefs.pyVer == 2):
|
||||
glyph = unichr(ord(self.startGlyph) + glyphIdx)
|
||||
else:
|
||||
glyph = chr(ord(self.startGlyph) + glyphIdx)
|
||||
addText(sourceGlyphLayer, glyph, posX, posY, self.textStyle)
|
||||
|
||||
return None
|
||||
|
||||
def effect(self):
|
||||
#TODO: Maybe validate if a template was already created
|
||||
rowCnt = self.options.rowCnt
|
||||
srcFontFamily = self.options.srcFontFamily
|
||||
fontType = self.options.fontType
|
||||
glyphCnt = self.options.glyphCnt
|
||||
rvGuides = self.options.rvGuides
|
||||
fontSize = self.options.fontSize
|
||||
strokeOpacity = self.options.strokeOpacity
|
||||
fillOpacity = self.options.fillOpacity
|
||||
spaceWidth = self.options.spaceWidth
|
||||
|
||||
self.createTTGlyphs = self.options.createGlyphs
|
||||
sg = self.options.startGlyph
|
||||
self.startGlyph = None
|
||||
if(len(sg) > 0):
|
||||
if(len(sg) == 4):
|
||||
try: self.startGlyph = eval("u'\\u" + sg + "'")
|
||||
except: pass
|
||||
if(self.startGlyph == None):
|
||||
self.startGlyph = getDecodedChars(sg)[0]
|
||||
else:
|
||||
self.startGlyph = 'A'
|
||||
|
||||
lineT = CommonDefs.lineT * fontSize
|
||||
|
||||
if('bold' in fontType):
|
||||
fontWeight = 'bold'
|
||||
else:
|
||||
fontWeight = 'normal'
|
||||
|
||||
if('italic' in fontType):
|
||||
fontStyle = 'italic'
|
||||
else:
|
||||
fontStyle = 'normal'
|
||||
|
||||
self.textStyle = {'font-family':srcFontFamily, 'font-size':str(fontSize),\
|
||||
'fill':'#000000', 'fill-opacity':str(fillOpacity), 'stroke':'#000000', 'stroke-width':str(lineT),\
|
||||
'stroke-opacity':str(strokeOpacity), 'font-style':fontStyle,'font-weight':fontWeight,\
|
||||
'text-align':'start'}
|
||||
|
||||
vgScaleFact = CommonDefs.vgScaleFact
|
||||
|
||||
fontName = 'NA'
|
||||
|
||||
extraInfo = getDefaultExtraInfo(fontName, fontSize)
|
||||
extraInfo[xSpaceROff] = spaceWidth
|
||||
|
||||
createTempl(self.addElem, self, extraInfo, rowCnt, glyphCnt, vgScaleFact, \
|
||||
rvGuides, lineT, newCallBackLayerName = 'Source Glyphs')
|
||||
|
||||
runEffect(CustStrokeFontTempl())
|
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 197 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 187 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 336 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 127 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 133 KiB |
After Width: | Height: | Size: 157 KiB |
After Width: | Height: | Size: 183 KiB |
After Width: | Height: | Size: 178 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 105 KiB |
@ -0,0 +1,97 @@
|
||||
Copyright (c) <dates>, <Copyright Holder> (<URL|email>),
|
||||
with Reserved Font Name <Reserved Font Name>.
|
||||
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>),
|
||||
with Reserved Font Name <additional Reserved Font Name>.
|
||||
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Custom Stroke Font - Synchronize Font List</name>
|
||||
<id>fablabchemnitz.de.stroke_font_creator.sync_stroke_font</id>
|
||||
<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">sync_stroke_font.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
'''
|
||||
Inkscape extension to synchronize the stroke font list with the generated data.
|
||||
|
||||
This effect updates the inx file of the effect that renders the text with the fonts
|
||||
from the data file.
|
||||
|
||||
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, os, sys
|
||||
from xml.dom.minidom import parse
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
from stroke_font_common import syncFontList, runEffect
|
||||
|
||||
class SyncFontListEffect(inkex.Effect):
|
||||
|
||||
def __init__(self):
|
||||
inkex.Effect.__init__(self)
|
||||
|
||||
def effect(self):
|
||||
extPath = os.path.dirname(os.path.abspath(__file__))
|
||||
syncFontList(extPath)
|
||||
|
||||
runEffect(SyncFontListEffect())
|