| 1 | """ |
|---|
| 2 | This module is the bridge between Python and the FDK. |
|---|
| 3 | It uses subprocess.Popen to create a process that |
|---|
| 4 | executes an FDK program. |
|---|
| 5 | """ |
|---|
| 6 | |
|---|
| 7 | |
|---|
| 8 | import sys |
|---|
| 9 | import os |
|---|
| 10 | import re |
|---|
| 11 | import tempfile |
|---|
| 12 | |
|---|
| 13 | # ---------------- |
|---|
| 14 | # Public Functions |
|---|
| 15 | # ---------------- |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | minMakeOTFVersion = (2, 0, 39) |
|---|
| 19 | minMakeOTFVersionRE = re.compile( |
|---|
| 20 | "\s*" |
|---|
| 21 | "makeotf" |
|---|
| 22 | "\s+" |
|---|
| 23 | "v" |
|---|
| 24 | "(\d+)" |
|---|
| 25 | "." |
|---|
| 26 | "(\d+)" |
|---|
| 27 | "." |
|---|
| 28 | "(\d+)" |
|---|
| 29 | ) |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | def 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 | |
|---|
| 65 | def 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 | |
|---|
| 97 | def 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 | |
|---|
| 111 | def 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 | |
|---|
| 148 | outlineCheckFirstLineRE = re.compile( |
|---|
| 149 | "Wrote fixed file" |
|---|
| 150 | ".+" |
|---|
| 151 | ) |
|---|
| 152 | |
|---|
| 153 | def 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 | |
|---|
| 223 | def 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 | |
|---|
| 233 | if sys.platform == "darwin": |
|---|
| 234 | _fdkToolDirectory = os.path.join(os.environ["HOME"], "bin/FDK/Tools/osx") |
|---|
| 235 | else: |
|---|
| 236 | _fdkToolDirectory = None |
|---|
| 237 | |
|---|
| 238 | def _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 | |
|---|
| 248 | def _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 | |
|---|