added stroke font creator

This commit is contained in:
Mario Voigt 2021-10-16 01:08:58 +02:00
parent e423e1e7ea
commit a6f397a8f8
40 changed files with 2319 additions and 0 deletions

View File

@ -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>

View File

@ -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())

View File

@ -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>

View File

@ -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.')

View 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"
]
}
]

View File

@ -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>

View File

@ -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())

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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())

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 197 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 72 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 221 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 240 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 187 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 125 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 72 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 157 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 183 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 178 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -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.

View File

@ -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>

View File

@ -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())