source: packages/ufo2fdk/trunk/Lib/ufo2fdk/fdkBridge.py @ 872

Revision 872, 7.8 KB checked in by tal, 22 months ago (diff)
Renamed a function and added an FDK bug workaround.
Line 
1"""
2This module is the bridge between Python and the FDK.
3It uses subprocess.Popen to create a process that
4executes an FDK program.
5"""
6
7
8import sys
9import os
10import re
11import tempfile
12
13# ----------------
14# Public Functions
15# ----------------
16
17
18minMakeOTFVersion = (2, 0, 39)
19minMakeOTFVersionRE = re.compile(
20    "\s*"
21    "makeotf"
22    "\s+"
23    "v"
24    "(\d+)"
25    "."
26    "(\d+)"
27    "."
28    "(\d+)"
29)
30
31
32def haveFDK():
33    """
34    This will return a bool indicating if the FDK
35    can be found. It searches for the FDK by using
36    *which* to find the commandline *makeotf*,
37    *checkoutlines* and *autohint* programs. If one
38    of those cannot be found, this FDK is considered
39    to be unavailable.
40    """
41    import subprocess
42    if _fdkToolDirectory is None:
43        return False
44    env = _makeEnviron()
45    for tool in ["makeotf", "checkoutlines", "autohint"]:
46        cmds = "which %s" % tool
47        popen = subprocess.Popen(cmds, stderr=subprocess.PIPE, stdout=subprocess.PIPE, env=env, shell=True)
48        popen.wait()
49        text = popen.stderr.read()
50        text += popen.stdout.read()
51        if not text:
52            return False
53    # now test to make sure that makeotf is new enough
54    help = _execute(["makeotf", "-h"])[1]
55    m = minMakeOTFVersionRE.match(help)
56    if m is None:
57        return False
58    v1 = int(m.group(1))
59    v2 = int(m.group(2))
60    v3 = int(m.group(3))
61    if (v1, v2, v3) < minMakeOTFVersion:
62        return False
63    return True
64
65def makeotf(outputPath, outlineSourcePath=None, featuresPath=None, glyphOrderPath=None, menuNamePath=None, fontInfoPath=None, releaseMode=False):
66    """
67    Run makeotf.
68    The arguments will be converted into arguments
69    for makeotf as follows:
70
71    =================  ===
72    outputPath         -o
73    outlineSourcePath  -f
74    featuresPath       -ff
75    glyphOrderPath     -gf
76    menuNamePath       -mf
77    fontInfoPath       -fi
78    releaseMode        -r
79    =================  ===
80    """
81    cmds = ["makeotf", "-o", outputPath]
82    if outlineSourcePath:
83        cmds.extend(["-f", outlineSourcePath])
84    if featuresPath:
85        cmds.extend(["-ff", featuresPath])
86    if glyphOrderPath:
87        cmds.extend(["-gf", glyphOrderPath])
88    if menuNamePath:
89        cmds.extend(["-mf", menuNamePath])
90    if fontInfoPath:
91        cmds.extend(["-fi", fontInfoPath])
92    if releaseMode:
93        cmds.append("-r")
94    stderr, stdout = _execute(cmds)
95    return stderr, stdout
96
97def autohint(fontPath):
98    """
99    Run autohint.
100    The following arguments will be passed to autohint.
101
102    * -nb
103    * -a
104    * -r
105    * -q
106    """
107    cmds = ["autohint", "-nb", "-a", "-r", "-q", fontPath]
108    stderr, stdout = _execute(cmds)
109    return stderr, stdout
110
111def checkOutlines(fontPath, removeOverlap=True, correctContourDirection=True):
112    """
113    Run checkOutlines.
114    The arguments will be converted into arguments
115    for checkOutlines as follows:
116
117    The following arguments will be passed to autohint.
118
119    =============================  ===
120    removeOverlap=False            -V
121    correctContourDirection=False  -O
122    =============================  ===
123
124    Additionally, the following arguments will be passed to checkOutlines.
125
126    * -e
127    * -k
128    """
129    cmds = ["checkoutlines", "-e", "-k"]
130    # checkoutlines seems to apply the contour direction correction
131    # before the overlap removal. that may alter the design of the
132    # glyph when self-intersecting contours are present. to get
133    # around this, do these separately. this is inefficient, but...
134    allStderr = []
135    allStdout = []
136    if removeOverlap:
137        c = cmds + ["-O", fontPath]
138        stderr, stdout = _execute(c)
139        allStderr.append(stderr)
140        allStdout.append(stdout)
141    if correctContourDirection:
142        c = cmds + ["-V", fontPath]
143        stderr, stdout = _execute(c)
144        allStderr.append(stderr)
145        allStdout.append(stdout)
146    return "\n".join(allStderr), "\n".join(allStdout)
147
148outlineCheckFirstLineRE = re.compile(
149    "Wrote fixed file"
150    ".+"
151)
152
153def checkOutlinesGlyph(glyph, contours, removeOverlap=True, correctContourDirection=True):
154    """
155    Run outlineCheck on one or more contours.
156    This will remove the original contours from
157    the given glyph, if a change was made. This
158    returns a boolean indicating if a change
159    was made or not.
160
161    The arguments will be converted into arguments
162    for outlineCheck as follows:
163
164    The following arguments will be passed to autohint.
165
166    =============================  ===
167    removeOverlap=False            -V
168    correctContourDirection=False  -O
169    =============================  ===
170
171    Additionally, the following arguments will be passed to outlineCheck.
172
173    * -e
174    * -k
175    """
176    from ufo2fdk.pens.bezPen import BezPen, drawBez
177    # write the bez
178    pen = BezPen(glyphSet=None)
179    for contour in contours:
180        contour.draw(pen)
181    inBez = pen.getBez()
182    # write the bez to a temp file
183    bezPathIn = tempfile.mkstemp()[1]
184    f = open(bezPathIn, "w")
185    f.write(inBez)
186    f.close()
187    # call the outlinecheck tool
188    cmds = ["outlinecheck", "-n", "-k"]
189    if not removeOverlap:
190        cmds.append("-V")
191    if not correctContourDirection:
192        cmds.append("-O")
193    cmds.append(bezPathIn)
194    _execute(cmds)
195    # load the new bez
196    bezPathOut = bezPathIn + ".new"
197    madeChange = False
198    if os.path.exists(bezPathOut):
199        f = open(bezPathOut, "r")
200        outBez = f.read()
201        f.close()
202        # remove irrelevant log at the top of the file
203        outBez = outBez.splitlines()
204        if outlineCheckFirstLineRE.match(outBez[0]):
205            outBez = outBez[1:]
206        outBez = "\n".join(outBez)
207        # apply only if the new bez is different than the old bez
208        if inBez != outBez:
209            # remove the old contours
210            for contour in contours:
211                glyph.removeContour(contour)
212            # write the bez back into the glyph
213            pen = glyph.getPen()
214            drawBez(outBez, pen)
215            madeChange = True
216    # remove files
217    for path in [bezPathIn, bezPathOut]:
218        if os.path.exists(path):
219            os.remove(path)
220    # return the change status
221    return madeChange
222
223def removeOverlap(glyph, contours):
224    from warnings import warn
225    warn(DeprecationWarning("Use checkOutlinesGlyph!"))
226    return checkOutlinesGlyph(glyph, contours, removeOverlap=True, correctContourDirection=False)
227
228
229# --------------
230# Internal Tools
231# --------------
232
233if sys.platform == "darwin":
234    _fdkToolDirectory = os.path.join(os.environ["HOME"], "bin/FDK/Tools/osx")
235else:
236    _fdkToolDirectory = None
237
238def _makeEnviron():
239    env = dict(os.environ)
240    if _fdkToolDirectory not in env["PATH"].split(":"):
241        env["PATH"] += (":%s" % _fdkToolDirectory)
242    kill = ["ARGVZERO", "EXECUTABLEPATH", "PYTHONHOME", "PYTHONPATH", "RESOURCEPATH"]
243    for key in kill:
244        if key in env:
245            del env[key]
246    return env
247
248def _execute(cmds):
249    import subprocess
250    # for some reason, autohint and/or checkoutlines
251    # locks up when subprocess.PIPE is given. subprocess
252    # requires a real file so StringIO is not acceptable
253    # here. thus, make a temporary file.
254    stderrPath = tempfile.mkstemp()[1]
255    stdoutPath = tempfile.mkstemp()[1]
256    stderrFile = open(stderrPath, "w")
257    stdoutFile = open(stdoutPath, "w")
258    # get the os.environ
259    env = _makeEnviron()
260    # make a string of escaped commands
261    cmds = subprocess.list2cmdline(cmds)
262    # go
263    popen = subprocess.Popen(cmds, stderr=stderrFile, stdout=stdoutFile, env=env, shell=True)
264    popen.wait()
265    # get the output
266    stderrFile.close()
267    stdoutFile.close()
268    stderrFile = open(stderrPath, "r")
269    stdoutFile = open(stdoutPath, "r")
270    stderr = stderrFile.read()
271    stdout = stdoutFile.read()
272    stderrFile.close()
273    stdoutFile.close()
274    # trash the temp files
275    os.remove(stderrPath)
276    os.remove(stdoutPath)
277    # done
278    return stderr, stdout
279
Note: See TracBrowser for help on using the repository browser.