├── .gitignore ├── .gitmodules ├── Anchors ├── AdjustAnchors.py ├── AnchorsInput.py ├── AnchorsOutput.py ├── MarkFeatureGenerator.py └── RemoveAnchorsFromSelectedGlyphs.py ├── Hinting └── AutoHint.py ├── Kerning ├── ExportClassKerningToUFO.py └── KernFeatureGenerator.py ├── LICENSE ├── MM Designs ├── InstanceGenerator.py └── SaveFilesForMakeInstances.py ├── README.md ├── TrueType ├── README.md ├── convertToTTF.py ├── inputTTHints.py ├── outputPPMs.py ├── outputTTHint_coords.py ├── outputTTHints.py ├── tthDupe.py └── tthDupe_coords.py ├── Type1 └── OutlineCheckDialog.py └── installFontLabMacros.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Modules"] 2 | path = Modules 3 | url = https://github.com/adobe-type-tools/python-modules 4 | -------------------------------------------------------------------------------- /Anchors/AdjustAnchors.py: -------------------------------------------------------------------------------- 1 | #FLM: Adjust Anchors 2 | 3 | __copyright__ = __license__ = """ 4 | Copyright (c) 2010-2012 Adobe Systems Incorporated. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | """ 23 | 24 | __doc__ = """ 25 | Adjust Anchors v1.2 - Jul 12 2012 26 | 27 | This script provides a UI for adjusting the position of anchors interactively. 28 | FontLab's own UI for ajusting anchors is too poor. 29 | Opening FontLab's Preview window and selecting the Anchors pane before running 30 | this script, will allow you to preview the adjustments even better. 31 | 32 | ================================================== 33 | Versions: 34 | v1.0 - Apr 29 2010 - Initial version. 35 | v1.1 - Jun 15 2012 - UI improvements. 36 | v1.2 - Jul 12 2012 - Fixed issue that affected single master fonts. 37 | 38 | """ 39 | 40 | listGlyphsSelected = [] 41 | def getgselectedglyphs(font, glyph, gindex): 42 | listGlyphsSelected.append(gindex) 43 | fl.ForSelected(getgselectedglyphs) 44 | 45 | def getMasterNames(masters, axes): 46 | global matrix 47 | masterNames = [] 48 | if masters > 1: 49 | for m in range(masters): 50 | mtx = matrix[m] 51 | masterName = '' 52 | for i in range(len(axes)): 53 | masterName += ' ' + axes[i][1] + str(mtx[i]) 54 | masterNames.append(masterName) 55 | return masterNames 56 | 57 | matrix = [ 58 | (0,0,0,0),(1,0,0,0),(0,1,0,0),(1,1,0,0),(0,0,1,0),(1,0,1,0),(0,1,1,0),(1,1,1,0), 59 | (0,0,0,1),(1,0,0,1),(0,1,0,1),(1,1,0,1),(0,0,1,1),(1,0,1,1),(0,1,1,1),(1,1,1,1) 60 | ] 61 | 62 | STYLE_RADIO = STYLE_CHECKBOX + cTO_CENTER 63 | 64 | def run(gIndex): 65 | masters = f[0].layers_number 66 | axes = f.axis 67 | masterNames = getMasterNames(masters, axes) 68 | increment = 0 69 | if len(axes) == 3: 70 | increment = 90 71 | elif len(axes) > 3: 72 | fl.Message("This macro does not support 4-axis fonts") 73 | return 74 | 75 | fl.EditGlyph(gIndex) # opens Glyph Window in case it's not open yet 76 | glyphBkupDict = {} # this will store a copy of the edited glyphs and will be used in case 'Cancel' is pressed 77 | 78 | class DialogClass: 79 | def __init__(self): 80 | self.d = Dialog(self) 81 | self.d.size = Point(660, 110 + 48*4 + increment) 82 | self.d.Center() 83 | self.d.title = 'Adjust Anchors' 84 | 85 | self.anchorList = [] 86 | self.anchorList_index = 0 87 | self.anchorList_selected = 0 88 | self.selectedAnchor = None 89 | 90 | self.glyph = f[gIndex] 91 | self.gIndex = gIndex 92 | self.gName = self.glyph.name 93 | self.gHasAnchors = 0 94 | self.glyphList = [] 95 | self.glyphList_index = 0 96 | self.glyphList_selected = 0 97 | self.selectedglyph = None 98 | 99 | self.k_BIG_SHIFT = 20 100 | self.k_MEDIUM_SHIFT = 5 101 | self.k_SMALL_SHIFT = 1 102 | 103 | self.Xshift = 0 104 | self.Yshift = 0 105 | self.Xorig = 0 106 | self.Yorig = 0 107 | self.Xfinal = 0 108 | self.Yfinal = 0 109 | 110 | self.RBmasterIndex = 0 111 | if fl.layer == 0: self.RBmaster0 = 1 112 | else: self.RBmaster0 = 0 113 | if fl.layer == 1: self.RBmaster1 = 1 114 | else: self.RBmaster1 = 0 115 | if fl.layer == 2: self.RBmaster2 = 1 116 | else: self.RBmaster2 = 0 117 | if fl.layer == 3: self.RBmaster3 = 1 118 | else: self.RBmaster3 = 0 119 | if fl.layer == 4: self.RBmaster4 = 1 120 | else: self.RBmaster4 = 0 121 | if fl.layer == 5: self.RBmaster5 = 1 122 | else: self.RBmaster5 = 0 123 | if fl.layer == 6: self.RBmaster6 = 1 124 | else: self.RBmaster6 = 0 125 | if fl.layer == 7: self.RBmaster7 = 1 126 | else: self.RBmaster7 = 0 127 | 128 | # Fill in the Anchor list 129 | for anchor in self.glyph.anchors: 130 | self.anchorList.append(anchor.name) 131 | 132 | # Fill in the Glyph list 133 | for g in f.glyphs: 134 | if len(g.anchors) > 0: 135 | self.glyphList.append(g.name) 136 | 137 | # Checks if the initially selected glyph has anchors 138 | if self.gName in self.glyphList: 139 | self.gHasAnchors = 1 140 | 141 | posy = 10 + 48*0 # (xTop , yTop , xBot , yBot) 142 | self.d.AddControl(BUTTONCONTROL, Rect(310, posy, 350, posy+40), 'Yplus5', STYLE_BUTTON, '+'+ str(self.k_MEDIUM_SHIFT)) 143 | 144 | posy = 10 + 24*1 145 | self.d.AddControl(LISTCONTROL, Rect( 10, posy, 150, posy+110), 'glyphList', STYLE_LIST, 'Glyphs') 146 | self.d.AddControl(LISTCONTROL, Rect(510, posy, 650, posy+110), 'anchorList', STYLE_LIST, 'Anchors') 147 | 148 | posy = 10 + 48*1 149 | self.d.AddControl(BUTTONCONTROL, Rect(310, posy, 350, posy+40), 'Yplus1', STYLE_BUTTON, '+'+ str(self.k_SMALL_SHIFT)) 150 | 151 | posy = 10 + 48*2 152 | self.d.AddControl(BUTTONCONTROL, Rect(160, posy, 200, posy+40), 'Xminus20', STYLE_BUTTON, '-'+ str(self.k_BIG_SHIFT)) 153 | self.d.AddControl(BUTTONCONTROL, Rect(210, posy, 250, posy+40), 'Xminus5', STYLE_BUTTON, '-'+ str(self.k_MEDIUM_SHIFT)) 154 | self.d.AddControl(BUTTONCONTROL, Rect(260, posy, 300, posy+40), 'Xminus1', STYLE_BUTTON, '-'+ str(self.k_SMALL_SHIFT)) 155 | self.d.AddControl(STATICCONTROL, Rect(310, posy, 323, posy+20), 'stat_label', STYLE_LABEL+cTO_CENTER, 'x:') 156 | self.d.AddControl(STATICCONTROL, Rect(323, posy, 360, posy+20), 'Xshift', STYLE_LABEL+cTO_CENTER) 157 | self.d.AddControl(STATICCONTROL, Rect(310, posy+20, 323, posy+40), 'stat_label', STYLE_LABEL+cTO_CENTER, 'y:') 158 | self.d.AddControl(STATICCONTROL, Rect(323, posy+20, 360, posy+40), 'Yshift', STYLE_LABEL+cTO_CENTER) 159 | self.d.AddControl(BUTTONCONTROL, Rect(360, posy, 400, posy+40), 'Xplus1', STYLE_BUTTON, '+'+ str(self.k_SMALL_SHIFT)) 160 | self.d.AddControl(BUTTONCONTROL, Rect(410, posy, 450, posy+40), 'Xplus5', STYLE_BUTTON, '+'+ str(self.k_MEDIUM_SHIFT)) 161 | self.d.AddControl(BUTTONCONTROL, Rect(460, posy, 500, posy+40), 'Xplus20', STYLE_BUTTON, '+'+ str(self.k_BIG_SHIFT)) 162 | 163 | for i in range(len(masterNames)): 164 | posy = 154 + 22*i 165 | self.d.AddControl(CHECKBOXCONTROL, Rect( 25, posy, 200, posy+20), 'RBmaster'+ str(i), STYLE_RADIO, masterNames[i]) 166 | 167 | posy = 10 + 48*3 168 | self.d.AddControl(BUTTONCONTROL, Rect(310, posy, 350, posy+40), 'Yminus1', STYLE_BUTTON, '-'+ str(self.k_SMALL_SHIFT)) 169 | self.d.AddControl(STATICCONTROL, Rect(528, posy, 650, posy+20), 'stat_label', STYLE_LABEL+cTO_CENTER, 'Original position') 170 | self.d.AddControl(STATICCONTROL, Rect(530, posy+20, 543, posy+40), 'stat_label', STYLE_LABEL+cTO_CENTER, 'x:') 171 | self.d.AddControl(STATICCONTROL, Rect(543, posy+20, 580, posy+40), 'Xorig', STYLE_LABEL+cTO_CENTER) 172 | self.d.AddControl(STATICCONTROL, Rect(590, posy+20, 603, posy+40), 'stat_label', STYLE_LABEL+cTO_CENTER, 'y:') 173 | self.d.AddControl(STATICCONTROL, Rect(603, posy+20, 640, posy+40), 'Yorig', STYLE_LABEL+cTO_CENTER) 174 | 175 | posy = 10 + 48*4 176 | self.d.AddControl(BUTTONCONTROL, Rect(310, posy, 350, posy+40), 'Yminus5', STYLE_BUTTON, '-'+ str(self.k_MEDIUM_SHIFT)) 177 | self.d.AddControl(STATICCONTROL, Rect(528, posy, 650, posy+20), 'stat_label', STYLE_LABEL+cTO_CENTER, 'Final position') 178 | self.d.AddControl(STATICCONTROL, Rect(530, posy+20, 543, posy+40), 'stat_label', STYLE_LABEL+cTO_CENTER, 'x:') 179 | self.d.AddControl(STATICCONTROL, Rect(543, posy+20, 580, posy+40), 'Xfinal', STYLE_LABEL+cTO_CENTER) 180 | self.d.AddControl(STATICCONTROL, Rect(590, posy+20, 603, posy+40), 'stat_label', STYLE_LABEL+cTO_CENTER, 'y:') 181 | self.d.AddControl(STATICCONTROL, Rect(603, posy+20, 640, posy+40), 'Yfinal', STYLE_LABEL+cTO_CENTER) 182 | 183 | 184 | #====== DIALOG FUNCTIONS ========= 185 | 186 | def on_Xminus20(self, code): 187 | if self.anchorList_selected: 188 | self.Xshift -= self.k_BIG_SHIFT 189 | self.d.PutValue('Xshift') 190 | self.updateXfinal() 191 | self.update_glyph() 192 | def on_Xminus5(self, code): 193 | if self.anchorList_selected: 194 | self.Xshift -= self.k_MEDIUM_SHIFT 195 | self.d.PutValue('Xshift') 196 | self.updateXfinal() 197 | self.update_glyph() 198 | def on_Xminus1(self, code): 199 | if self.anchorList_selected: 200 | self.Xshift -= self.k_SMALL_SHIFT 201 | self.d.PutValue('Xshift') 202 | self.updateXfinal() 203 | self.update_glyph() 204 | def on_Xplus1(self, code): 205 | if self.anchorList_selected: 206 | self.Xshift += self.k_SMALL_SHIFT 207 | self.d.PutValue('Xshift') 208 | self.updateXfinal() 209 | self.update_glyph() 210 | def on_Xplus5(self, code): 211 | if self.anchorList_selected: 212 | self.Xshift += self.k_MEDIUM_SHIFT 213 | self.d.PutValue('Xshift') 214 | self.updateXfinal() 215 | self.update_glyph() 216 | def on_Xplus20(self, code): 217 | if self.anchorList_selected: 218 | self.Xshift += self.k_BIG_SHIFT 219 | self.d.PutValue('Xshift') 220 | self.updateXfinal() 221 | self.update_glyph() 222 | 223 | def on_Yminus5(self, code): 224 | if self.anchorList_selected: 225 | self.Yshift -= self.k_MEDIUM_SHIFT 226 | self.d.PutValue('Yshift') 227 | self.updateYfinal() 228 | self.update_glyph() 229 | def on_Yminus1(self, code): 230 | if self.anchorList_selected: 231 | self.Yshift -= self.k_SMALL_SHIFT 232 | self.d.PutValue('Yshift') 233 | self.updateYfinal() 234 | self.update_glyph() 235 | def on_Yplus1(self, code): 236 | if self.anchorList_selected: 237 | self.Yshift += self.k_SMALL_SHIFT 238 | self.d.PutValue('Yshift') 239 | self.updateYfinal() 240 | self.update_glyph() 241 | def on_Yplus5(self, code): 242 | if self.anchorList_selected: 243 | self.Yshift += self.k_MEDIUM_SHIFT 244 | self.d.PutValue('Yshift') 245 | self.updateYfinal() 246 | self.update_glyph() 247 | 248 | def on_glyphList(self, code): 249 | self.glyphList_selected = 1 250 | self.gHasAnchors = 1 251 | self.d.GetValue('glyphList') 252 | self.gName = self.glyphList[self.glyphList_index] # Name of the glyph selected on the glyph list 253 | self.gIndex = f.FindGlyph(self.gName) 254 | fl.iglyph = self.gIndex # Switch the glyph on the Glyph Window 255 | self.glyph = f[self.gIndex] 256 | self.updateAnchorsList() 257 | self.resetDialogValues() 258 | 259 | def on_anchorList(self, code): 260 | self.anchorList_selected = 1 261 | self.d.GetValue('anchorList') 262 | self.updateDialogValues() 263 | 264 | def on_RBmaster0(self, code): self.updateRBmaster(0) 265 | def on_RBmaster1(self, code): self.updateRBmaster(1) 266 | def on_RBmaster2(self, code): self.updateRBmaster(2) 267 | def on_RBmaster3(self, code): self.updateRBmaster(3) 268 | def on_RBmaster4(self, code): self.updateRBmaster(4) 269 | def on_RBmaster5(self, code): self.updateRBmaster(5) 270 | def on_RBmaster6(self, code): self.updateRBmaster(6) 271 | def on_RBmaster7(self, code): self.updateRBmaster(7) 272 | 273 | def on_ok(self, code): 274 | return 1 275 | 276 | 277 | #====== RESET FUNCTIONS ========= 278 | 279 | def resetDialogValues(self): 280 | self.resetXorig() 281 | self.resetYorig() 282 | self.resetXshift() 283 | self.resetYshift() 284 | self.resetXfinal() 285 | self.resetYfinal() 286 | 287 | def resetXorig(self): 288 | self.Xorig = 0 289 | self.d.PutValue('Xorig') 290 | 291 | def resetYorig(self): 292 | self.Yorig = 0 293 | self.d.PutValue('Yorig') 294 | 295 | def resetXshift(self): 296 | self.Xshift = 0 297 | self.d.PutValue('Xshift') 298 | 299 | def resetYshift(self): 300 | self.Yshift = 0 301 | self.d.PutValue('Yshift') 302 | 303 | def resetXfinal(self): 304 | self.Xfinal = 0 305 | self.d.PutValue('Xfinal') 306 | 307 | def resetYfinal(self): 308 | self.Yfinal = 0 309 | self.d.PutValue('Yfinal') 310 | 311 | 312 | #====== UPDATE FUNCTIONS ========= 313 | 314 | def updateRBmaster(self, newIndex): 315 | self.RBmasterIndex = newIndex 316 | if self.RBmasterIndex == 0: self.RBmaster0 = 1 317 | else: self.RBmaster0 = 0 318 | if self.RBmasterIndex == 1: self.RBmaster1 = 1 319 | else: self.RBmaster1 = 0 320 | if self.RBmasterIndex == 2: self.RBmaster2 = 1 321 | else: self.RBmaster2 = 0 322 | if self.RBmasterIndex == 3: self.RBmaster3 = 1 323 | else: self.RBmaster3 = 0 324 | if self.RBmasterIndex == 4: self.RBmaster4 = 1 325 | else: self.RBmaster4 = 0 326 | if self.RBmasterIndex == 5: self.RBmaster5 = 1 327 | else: self.RBmaster5 = 0 328 | if self.RBmasterIndex == 6: self.RBmaster6 = 1 329 | else: self.RBmaster6 = 0 330 | if self.RBmasterIndex == 7: self.RBmaster7 = 1 331 | else: self.RBmaster7 = 0 332 | for v in ['RBmaster0','RBmaster1','RBmaster2','RBmaster3','RBmaster4','RBmaster5','RBmaster6','RBmaster7']: 333 | self.d.PutValue(v) 334 | fl.layer = self.RBmasterIndex 335 | if self.gHasAnchors and self.anchorList_selected: 336 | self.updateDialogValues() 337 | 338 | def updateAnchorsList(self): 339 | self.anchorList = [] 340 | for anchor in self.glyph.anchors: 341 | self.anchorList.append(anchor.name) 342 | self.d.PutValue('anchorList') 343 | self.anchorList_selected = 0 344 | self.selectedAnchor = None 345 | 346 | def updateDialogValues(self): 347 | self.selectedAnchor = self.glyph.anchors[self.anchorList_index].Layer(fl.layer) 348 | self.updateXorig(self.selectedAnchor.x) 349 | self.updateYorig(self.selectedAnchor.y) 350 | self.resetXshift() 351 | self.resetYshift() 352 | self.updateXfinal() 353 | self.updateYfinal() 354 | 355 | def updateXorig(self, pos): 356 | self.Xorig = pos 357 | self.d.PutValue('Xorig') 358 | 359 | def updateYorig(self, pos): 360 | self.Yorig = pos 361 | self.d.PutValue('Yorig') 362 | 363 | def updateXfinal(self): 364 | if self.anchorList_selected: 365 | self.Xfinal = self.Xorig + self.Xshift 366 | self.d.PutValue('Xfinal') 367 | 368 | def updateYfinal(self): 369 | if self.anchorList_selected: 370 | self.Yfinal = self.Yorig + self.Yshift 371 | self.d.PutValue('Yfinal') 372 | 373 | 374 | def update_glyph(self): 375 | if self.anchorList_selected: 376 | if self.gIndex not in glyphBkupDict: 377 | # print "Made backup copy of '%s'" % self.glyph.name 378 | glyphBkupDict[self.gIndex] = Glyph(f[self.gIndex]) 379 | fl.SetUndo(self.gIndex) 380 | x = self.Xfinal 381 | y = self.Yfinal 382 | anchorPosition = Point(x, y) 383 | anchorIndex = self.anchorList_index 384 | anchor = self.glyph.anchors[anchorIndex] 385 | # In single master fonts the adjustment of the anchors cannot be handled by the codepath used for multiple 386 | # master fonts, because the UI gets updated but the changes are not stored in the VFB file upon saving. 387 | if masters == 1: 388 | anchor.x = x 389 | anchor.y = y 390 | else: 391 | anchor.SetLayer(fl.layer, anchorPosition) 392 | fl.UpdateGlyph(self.gIndex) 393 | 394 | def Run(self): 395 | return self.d.Run() 396 | 397 | 398 | d = DialogClass() 399 | 400 | if d.Run() == 1: 401 | f.modified = 1 402 | else: 403 | for gID in glyphBkupDict: 404 | f[gID] = glyphBkupDict[gID] 405 | fl.UpdateGlyph(gID) 406 | f.modified = 0 407 | 408 | 409 | if __name__ == "__main__": 410 | f = fl.font 411 | gIndex = fl.iglyph 412 | if f is None: 413 | fl.Message('No font opened') 414 | elif gIndex < 0: 415 | if len(listGlyphsSelected) == 0: 416 | fl.Message('Glyph selection is not valid') 417 | else: 418 | gIndex = listGlyphsSelected[0] 419 | run(gIndex) 420 | else: 421 | run(gIndex) 422 | -------------------------------------------------------------------------------- /Anchors/AnchorsInput.py: -------------------------------------------------------------------------------- 1 | #FLM: Anchors Input 2 | 3 | __copyright__ = __license__ = """ 4 | Copyright (c) 2010-2012 Adobe Systems Incorporated. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | """ 23 | 24 | __doc__ = """ 25 | Anchors Input v1.2 - Jun 15 2012 26 | 27 | Adds anchors to the glyphs by reading the data from external text file(s) named 28 | anchors_X (where X represents the index of the font's master). 29 | The simple-text file(s) containing the anchor data must have four tab-separated 30 | columns that follow this format: 31 | glyphName anchorName anchorXposition anchorYposition 32 | 33 | ================================================== 34 | Versions: 35 | v1.0 - Apr 29 2010 - Initial version. 36 | v1.0.1 - Apr 29 2010 - Make sure that the glyph listed in the anchors_x file exists in the font. 37 | v1.0.2 - Jul 18 2011 - Make sure that there are no two anchors with the same name. 38 | v1.1 - Ago 03 2011 - Improvements to account for MM fonts. 39 | v1.1.1 - Apr 02 2012 - Check for the presence of the anchors file first. 40 | v1.2 - Jun 15 2012 - Added undo support. Improved the handling of errors in the input files. 41 | 42 | """ 43 | 44 | import os, time 45 | 46 | 47 | class myAnchor: 48 | def __init__(self, parent, name, x=0, y=0): 49 | self.parent = parent 50 | self.name = name 51 | self.x = x 52 | self.y = y 53 | 54 | 55 | class myAnchorLayers: 56 | def __init__(self, layers): 57 | self.layers = layers 58 | self.struct = [[] for i in range(self.layers)] # This way the multidimensional list is created without referencing 59 | 60 | def addToLayers(self, anchorsList): 61 | for i in range(self.layers): 62 | self.struct[i].append(anchorsList[i]) 63 | 64 | def addAnchorToLayer(self, anchor, layerIndex): 65 | self.struct[layerIndex].append(anchor) 66 | 67 | def getLayer(self, layerIndex): 68 | return self.struct[layerIndex] 69 | 70 | def outputLayer(self, num): 71 | self.anchorsList = [] 72 | for anchor in self.struct[num]: 73 | self.anchorEntry = "%s\t%s\t%d\t%d\n" % (anchor.parent, anchor.name, anchor.x, anchor.y) 74 | self.anchorsList.append(self.anchorEntry) 75 | return self.anchorsList 76 | 77 | 78 | def readFile(filePath): 79 | file = open(filePath, 'r') 80 | fileContent = file.read().splitlines() 81 | file.close() 82 | return fileContent 83 | 84 | 85 | def findAnchorIndex(anchorName, glyph): 86 | index = None 87 | for i in range(len(glyph.anchors)): 88 | if glyph.anchors[i].name == anchorName: 89 | index = i 90 | return index 91 | return index 92 | 93 | 94 | def run(): 95 | print "Working..." 96 | # Initialize object 97 | anchorsData = myAnchorLayers(numMasters) 98 | 99 | 100 | # Read files 101 | for index in range(numMasters): 102 | filePath = "anchors_%s" % index 103 | 104 | if not os.path.isfile(filePath): 105 | print "ERROR: File %s not found." % filePath 106 | return 107 | 108 | anchorLayer = readFile(filePath) 109 | 110 | for lineIndex in range(len(anchorLayer)): 111 | anchorValuesList = anchorLayer[lineIndex].split('\t') 112 | if len(anchorValuesList) != 4: # Four columns: glyph name, anchor name, anchor X postion, anchor Y postion 113 | print "ERROR: Line #%d does not have 4 columns. Skipped." % (lineIndex +1) 114 | continue 115 | 116 | glyphName = anchorValuesList[0] 117 | anchorName = anchorValuesList[1] 118 | 119 | if not len(glyphName) or not len(anchorName): 120 | print "ERROR: Line #%d has no glyph name or no anchor name. Skipped." % (lineIndex +1) 121 | continue 122 | 123 | try: 124 | anchorX = int(anchorValuesList[2]) 125 | anchorY = int(anchorValuesList[3]) 126 | except: 127 | print "ERROR: Line #%d has an invalid anchor position value. Skipped." % (lineIndex +1) 128 | continue 129 | 130 | newAnchor = myAnchor(glyphName, anchorName, anchorX, anchorY) 131 | anchorsData.addAnchorToLayer(newAnchor, index) 132 | 133 | 134 | # Remove all anchors 135 | for glyph in f.glyphs: 136 | if len(glyph.anchors) > 0: 137 | anchorIndexList = range(len(glyph.anchors)) 138 | anchorIndexList.reverse() 139 | for i in anchorIndexList: # delete from last to first 140 | del glyph.anchors[i] 141 | fl.UpdateGlyph(glyph.index) 142 | 143 | # Add all anchors 144 | for master in range(numMasters): 145 | anchorList = anchorsData.getLayer(master) 146 | 147 | for anchor in anchorList: 148 | # Make sure that the glyph exists 149 | if fl.font.FindGlyph(anchor.parent) != -1: 150 | glyph = f[anchor.parent] 151 | else: 152 | print "ERROR: Glyph %s not found in the font." % anchor.parent 153 | continue 154 | 155 | # Create the anchor in case it doesn't exist already 156 | if not glyph.FindAnchor(anchor.name): 157 | newAnchor = Anchor(anchor.name, anchor.x, anchor.y) 158 | glyph.anchors.append(newAnchor) 159 | fl.SetUndo(glyph.index) 160 | else: 161 | if master == 0: # check only on the first master (since the names of the anchors are the same in all masters) 162 | print "WARNING: Anchor exists already: %s\t%s" % (anchor.parent, anchor.name) 163 | 164 | # Reposition the anchor only in the masters other than the first 165 | if master > 0: 166 | anchorIndex = findAnchorIndex(anchor.name, glyph) 167 | glyph.anchors[anchorIndex].Layer(master).x = anchor.x 168 | glyph.anchors[anchorIndex].Layer(master).y = anchor.y 169 | 170 | #glyph.mark = 125 171 | fl.UpdateGlyph(glyph.index) 172 | 173 | 174 | f.modified = 1 175 | print 'Done! (%s)' % time.strftime("%H:%M:%S", time.localtime()) 176 | 177 | 178 | if __name__ == "__main__": 179 | f = fl.font 180 | if len(f): 181 | numMasters = f[0].layers_number 182 | 183 | if (f.file_name): 184 | fpath, fname = os.path.split(f.file_name) # The current file and path 185 | os.chdir(fpath) # Change current directory to location of open font 186 | run() 187 | else: 188 | print "The font needs to be saved first!" 189 | else: 190 | print "Font has no glyphs!" 191 | -------------------------------------------------------------------------------- /Anchors/AnchorsOutput.py: -------------------------------------------------------------------------------- 1 | #FLM: Anchors Output 2 | 3 | __copyright__ = __license__ = """ 4 | Copyright (c) 2010-2012 Adobe Systems Incorporated. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | """ 23 | 24 | __doc__ = """ 25 | Anchors Output v1.1 - Jun 15 2012 26 | 27 | Outputs all the anchor data to text file(s) named anchors_X (where X represents 28 | the index of the font's master). 29 | 30 | ================================================== 31 | Versions: 32 | v1.0 - Apr 29 2010 - Initial version. 33 | v1.1 - Jun 15 2012 - Skip nameless anchors. Don't output files if font has no anchors. 34 | 35 | """ 36 | 37 | import os 38 | 39 | 40 | class myAnchor: 41 | def __init__(self, parent, name, x=0, y=0): 42 | self.parent = parent 43 | self.name = name 44 | self.x = x 45 | self.y = y 46 | 47 | 48 | class myAnchorLayers: 49 | def __init__(self, layers): 50 | self.layers = layers 51 | self.struct = [[] for i in range(self.layers)] # This way the multidimensional list is created without referencing 52 | 53 | def addToLayers(self, anchorsList): 54 | for i in range(self.layers): 55 | self.struct[i].append(anchorsList[i]) 56 | 57 | def outputLayer(self, num): 58 | self.anchorsList = [] 59 | for anchor in self.struct[num]: 60 | self.anchorEntry = "%s\t%s\t%d\t%d\n" % (anchor.parent, anchor.name, anchor.x, anchor.y) 61 | self.anchorsList.append(self.anchorEntry) 62 | return self.anchorsList 63 | 64 | 65 | def run(): 66 | # Initialize object 67 | anchorsData = myAnchorLayers(numMasters) 68 | 69 | # Collect anchors data 70 | for glyph in f.glyphs: 71 | glyphName = glyph.name 72 | 73 | for anchorIndex in range(len(glyph.anchors)): 74 | anchorName = glyph.anchors[anchorIndex].name 75 | 76 | # Skip nameless anchors 77 | if not len(anchorName): 78 | print "ERROR: Glyph %s has a nameless anchor. Skipped." % glyphName 79 | continue 80 | 81 | tempSlice = [] 82 | 83 | for masterIndex in range(numMasters): 84 | point = glyph.anchors[anchorIndex].Layer(masterIndex) 85 | anchorX, anchorY = point.x, point.y 86 | newAnchor = myAnchor(glyphName, anchorName, anchorX, anchorY) 87 | tempSlice.append(newAnchor) 88 | 89 | anchorsData.addToLayers(tempSlice) 90 | 91 | if not len(anchorsData.outputLayer(0)): 92 | print "The font has no anchors!" 93 | return 94 | 95 | # Write files: 96 | for layer in range(numMasters): 97 | filename = "anchors_%s" % layer 98 | print "Writing file %s..." % filename 99 | outfile = open(filename, 'w') 100 | for anchorLine in anchorsData.outputLayer(layer): 101 | outfile.write(anchorLine) 102 | outfile.close() 103 | 104 | print 'Done!' 105 | 106 | 107 | if __name__ == "__main__": 108 | f = fl.font 109 | if len(f): 110 | numMasters = f[0].layers_number 111 | 112 | if (f.file_name): 113 | fpath, fname = os.path.split(f.file_name) # The current file and path 114 | os.chdir(fpath) # Change current directory to location of open font 115 | run() 116 | else: 117 | print "The font needs to be saved first!" 118 | else: 119 | print "Font has no glyphs!" 120 | -------------------------------------------------------------------------------- /Anchors/MarkFeatureGenerator.py: -------------------------------------------------------------------------------- 1 | #FLM: Mark Feature Generator 2 | 3 | ################################################### 4 | ### THE VALUES BELOW CAN BE EDITED AS NEEDED ###### 5 | ################################################### 6 | 7 | kInstancesDataFileName = "instances" 8 | kPrefsFileName = "MarkFeatureGenerator.prefs" 9 | 10 | ################################################### 11 | 12 | __copyright__ = __license__ = """ 13 | Copyright (c) 2010-2013 Adobe Systems Incorporated. All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a 16 | copy of this software and associated documentation files (the "Software"), 17 | to deal in the Software without restriction, including without limitation 18 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 19 | and/or sell copies of the Software, and to permit persons to whom the 20 | Software is furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 31 | DEALINGS IN THE SOFTWARE. 32 | """ 33 | 34 | __doc__ = """ 35 | Mark Feature Generator v1.3.2 - Mar 10 2013 36 | 37 | This script will generate a set of "features.mark" files from the mark data (anchors 38 | and combining marks class) of a Multiple Master (MM) FontLab font, or one "features.mark" 39 | file if the font is Single Master (SM). The "features.mark" file is a text file containing 40 | the font's mark attachment data in features-file definition syntax. 41 | 42 | The script can also generate "feature.mkmk" file(s). 43 | 44 | For information about how the "features.mark/mkmk" files are created, please read the 45 | documentation in the WriteFeaturesMarkFDK.py module that can be found 46 | in FontLab/Studio5/Macros/System/Modules/ 47 | 48 | For information on how to format the "instances" file, please read the documentation in the 49 | InstanceGenerator.py script. 50 | 51 | To access the script's options, hold down the CONTROL key while clicking on the play 52 | button to run the script. 53 | 54 | VERY IMPORTANT: For this script to work, all the combining mark glyphs 55 | must be added to an OpenType class named 'COMBINING_MARKS'. 56 | 57 | If the font has ligatures and the ligatures have anchors, the ligature glyphs have to be put in 58 | OpenType classes named "LIGATURES_WITH_X_COMPONENTS", where 'X' should be replaced by a number 59 | between 2 and 9 (inclusive). Additionally, the names of the anchors used on the ligature glyphs 60 | need to have a tag (e.g. 1ST, 2ND) which is used for corresponding the anchors with the correct 61 | ligature component. Keep in mind that in Right-To-Left scripts, the first component of the ligature 62 | is the one on the right side of the glyph. 63 | 64 | The script will first show a file selection dialog for choosing a folder. The "features.mark" 65 | file(s) will be written to that folder, if the font is SM, or to a sub-directory path 66 | /, if the font is MM. In this case, the face name is derived by 67 | taking the part of the font's PostScript name after the hyphen, or "Regular" is there is no 68 | hyphen. (e.g. if the font's PostScript name is MyFont-BoldItalic, the folder will be named 69 | "BoldItalic") 70 | 71 | If the font is MM, the script requires a file, named "instances", which contains all the 72 | instance-specific values. The "instances" file must be a simple text file, located in the 73 | same folder as the MM FontLab file. 74 | 75 | ================================================== 76 | Versions: 77 | v1.0 - Feb 15 2010 - Initial release 78 | v1.1 - Feb 19 2010 - Added the option to generate 'mkmk' feature 79 | v1.2 - Apr 21 2011 - Added the option of writing the mark classes in a separate file 80 | v1.3 - Jun 15 2012 - Added the option to output the lookups in the format required for Indian scripts. 81 | v1.3.1 - Jul 19 2012 - Changed the description of one of the options in the UI. 82 | v1.3.2 - Mar 10 2013 - Minor improvements. 83 | 84 | """ 85 | 86 | import os, sys, re, copy, math, time 87 | 88 | try: 89 | from AdobeFontLabUtils import checkControlKeyPress, checkShiftKeyPress 90 | import WriteFeaturesMarkFDK 91 | except ImportError,e: 92 | print "Failed to find the Adobe FDK support scripts." 93 | print "Please run the script FDK/Tools/FontLab/installFontLabMacros.py script, and try again." 94 | print "Current directory: ", os.path.abspath(os.getcwd()) 95 | print "Current list of search paths for modules: " 96 | import pprint 97 | pprint.pprint(sys.path) 98 | raise e 99 | 100 | kFieldsKey = "#KEYS:" 101 | kFamilyName = "FamilyName" 102 | kFontName = "FontName" 103 | kFullName = "FullName" 104 | kWeight = "Weight" 105 | kCoordsKey = "Coords" 106 | kIsBoldKey = "IsBold" # This is changed to kForceBold in the instanceDict when reading in the instance file. 107 | kForceBold = "ForceBold" 108 | kIsItalicKey = "IsItalic" 109 | kExceptionSuffixes = "ExceptionSuffixes" 110 | kExtraGlyphs = "ExtraGlyphs" 111 | 112 | kFixedFieldKeys = { 113 | # field index: key name 114 | 0:kFamilyName, 115 | 1:kFontName, 116 | 2:kFullName, 117 | 3:kWeight, 118 | 4:kCoordsKey, 119 | 5:kIsBoldKey, 120 | } 121 | 122 | kNumFixedFields = len(kFixedFieldKeys) 123 | 124 | kBlueScale = "BlueScale" 125 | kBlueShift = "BlueShift" 126 | kBlueFuzz = "BlueFuzz" 127 | kBlueValues = "BlueValues" 128 | kOtherBlues = "OtherBlues" 129 | kFamilyBlues = "FamilyBlues" 130 | kFamilyOtherBlues = "FamilyOtherBlues" 131 | kStdHW = "StdHW" 132 | kStdVW = "StdVW" 133 | kStemSnapH = "StemSnapH" 134 | kStemSnapV = "StemSnapV" 135 | 136 | kAlignmentZonesKeys = [kBlueValues, kOtherBlues, kFamilyBlues, kFamilyOtherBlues] 137 | kTopAlignZonesKeys = [kBlueValues, kFamilyBlues] 138 | kMaxTopZonesSize = 14 # 7 zones 139 | kBotAlignZonesKeys = [kOtherBlues, kFamilyOtherBlues] 140 | kMaxBotZonesSize = 10 # 5 zones 141 | kStdStemsKeys = [kStdHW, kStdVW] 142 | kMaxStdStemsSize = 1 143 | kStemSnapKeys = [kStemSnapH, kStemSnapV] 144 | kMaxStemSnapSize = 12 # including StdStem 145 | 146 | 147 | class ParseError(ValueError): 148 | pass 149 | 150 | 151 | def validateArrayValues(arrayList, valuesMustBePositive): 152 | for i in range(len(arrayList)): 153 | try: 154 | arrayList[i] = eval(arrayList[i]) 155 | except (NameError, SyntaxError): 156 | return 157 | if valuesMustBePositive: 158 | if arrayList[i] < 0: 159 | return 160 | return arrayList 161 | 162 | 163 | def readInstanceFile(instancesFilePath): 164 | f = open(instancesFilePath, "rt") 165 | data = f.read() 166 | f.close() 167 | 168 | lines = data.splitlines() 169 | 170 | i = 0 171 | parseError = 0 172 | keyDict = copy.copy(kFixedFieldKeys) 173 | numKeys = kNumFixedFields 174 | numLines = len(lines) 175 | instancesList = [] 176 | 177 | for i in range(numLines): 178 | line = lines[i] 179 | 180 | # Skip over blank lines 181 | line2 = line.strip() 182 | if not line2: 183 | continue 184 | 185 | # Get rid of all comments. If we find a key definition comment line, parse it. 186 | commentIndex = line.find('#') 187 | if commentIndex >= 0: 188 | if line.startswith(kFieldsKey): 189 | if instancesList: 190 | print "ERROR: Header line (%s) must preceed a data line." % kFieldsKey 191 | raise ParseError 192 | # parse the line with the field names. 193 | line = line[len(kFieldsKey):] 194 | line = line.strip() 195 | keys = line.split('\t') 196 | keys = map(lambda name: name.strip(), keys) 197 | numKeys = len(keys) 198 | k = kNumFixedFields 199 | while k < numKeys: 200 | keyDict[k] = keys[k] 201 | k +=1 202 | continue 203 | else: 204 | line = line[:commentIndex] 205 | continue 206 | 207 | # Must be a data line. 208 | fields = line.split('\t') 209 | fields = map(lambda datum: datum.strip(), fields) 210 | numFields = len(fields) 211 | if (numFields != numKeys): 212 | print "ERROR: In line %s, the number of fields %s does not match the number of key names %s (FamilyName, FontName, FullName, Weight, Coords, IsBold)." % (i+1, numFields, numKeys) 213 | parseError = 1 214 | continue 215 | 216 | instanceDict= {} 217 | #Build a dict from key to value. Some kinds of values needs special processing. 218 | for k in range(numFields): 219 | key = keyDict[k] 220 | field = fields[k] 221 | if not field: 222 | continue 223 | if field in ["Default", "None", "FontBBox"]: 224 | # FontBBox is no longer supported - I calculate the real 225 | # instance fontBBox from the glyph metrics instead, 226 | continue 227 | if key == kFontName: 228 | value = field 229 | elif key in [kExtraGlyphs, kExceptionSuffixes]: 230 | value = eval(field) 231 | elif key in [kIsBoldKey, kIsItalicKey, kCoordsKey]: 232 | try: 233 | value = eval(field) # this works for all three fields. 234 | 235 | if key == kIsBoldKey: # need to convert to Type 1 field key. 236 | instanceDict[key] = value 237 | # add kForceBold key. 238 | key = kForceBold 239 | if value == 1: 240 | value = "true" 241 | else: 242 | value = "false" 243 | elif key == kIsItalicKey: 244 | if value == 1: 245 | value = "true" 246 | else: 247 | value = "false" 248 | elif key == kCoordsKey: 249 | if type(value) == type(0): 250 | value = (value,) 251 | except (NameError, SyntaxError): 252 | print "ERROR: In line %s, the %s field has an invalid value." % (i+1, key) 253 | parseError = 1 254 | continue 255 | 256 | elif field[0] in ["[","{"]: # it is a Type 1 array value. Turn it into a list and verify that there's an even number of values for the alignment zones 257 | value = field[1:-1].split() # Remove the begin and end brackets/braces, and make a list 258 | 259 | if key in kAlignmentZonesKeys: 260 | if len(value) % 2 != 0: 261 | print "ERROR: In line %s, the %s field does not have an even number of values." % (i+1, key) 262 | parseError = 1 263 | continue 264 | 265 | if key in kTopAlignZonesKeys: # The Type 1 spec only allows 7 top zones (7 pairs of values) 266 | if len(value) > kMaxTopZonesSize: 267 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxTopZonesSize) 268 | parseError = 1 269 | continue 270 | else: 271 | newArray = validateArrayValues(value, False) # False = values do NOT have to be all positive 272 | if newArray: 273 | value = newArray 274 | else: 275 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 276 | parseError = 1 277 | continue 278 | currentArray = value[:] # make copy, not reference 279 | value.sort() 280 | if currentArray != value: 281 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 282 | 283 | if key in kBotAlignZonesKeys: # The Type 1 spec only allows 5 top zones (5 pairs of values) 284 | if len(value) > kMaxBotZonesSize: 285 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxBotZonesSize) 286 | parseError = 1 287 | continue 288 | else: 289 | newArray = validateArrayValues(value, False) # False = values do NOT have to be all positive 290 | if newArray: 291 | value = newArray 292 | else: 293 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 294 | parseError = 1 295 | continue 296 | currentArray = value[:] # make copy, not reference 297 | value.sort() 298 | if currentArray != value: 299 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 300 | 301 | if key in kStdStemsKeys: 302 | if len(value) > kMaxStdStemsSize: 303 | print "ERROR: In line %s, the %s field can only have %d value." % (i+1, key, kMaxStdStemsSize) 304 | parseError = 1 305 | continue 306 | else: 307 | newArray = validateArrayValues(value, True) # True = all values must be positive 308 | if newArray: 309 | value = newArray 310 | else: 311 | print "ERROR: In line %s, the %s field has an invalid value." % (i+1, key) 312 | parseError = 1 313 | continue 314 | 315 | if key in kStemSnapKeys: # The Type 1 spec only allows 12 stem widths, including 1 standard stem 316 | if len(value) > kMaxStemSnapSize: 317 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxStemSnapSize) 318 | parseError = 1 319 | continue 320 | else: 321 | newArray = validateArrayValues(value, True) # True = all values must be positive 322 | if newArray: 323 | value = newArray 324 | else: 325 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 326 | parseError = 1 327 | continue 328 | currentArray = value[:] # make copy, not reference 329 | value.sort() 330 | if currentArray != value: 331 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 332 | else: 333 | # either a single number or a string. 334 | if re.match(r"^[-.\d]+$", field): 335 | value = field #it is a Type 1 number. Pass as is, as a string. 336 | else: 337 | value = field 338 | 339 | instanceDict[key] = value 340 | 341 | if (kStdHW in instanceDict and kStemSnapH not in instanceDict) or (kStdHW not in instanceDict and kStemSnapH in instanceDict): 342 | print "ERROR: In line %s, either the %s value or the %s values are missing or were invalid." % (i+1, kStdHW, kStemSnapH) 343 | parseError = 1 344 | elif (kStdHW in instanceDict and kStemSnapH in instanceDict): # cannot be just 'else' because it will generate a 'KeyError' when these hinting parameters are not provided in the 'instances' file 345 | if instanceDict[kStemSnapH][0] != instanceDict[kStdHW][0]: 346 | print "ERROR: In line %s, the first value in %s must be the same as the %s value." % (i+1, kStemSnapH, kStdHW) 347 | parseError = 1 348 | 349 | if (kStdVW in instanceDict and kStemSnapV not in instanceDict) or (kStdVW not in instanceDict and kStemSnapV in instanceDict): 350 | print "ERROR: In line %s, either the %s value or the %s values are missing or were invalid." % (i+1, kStdVW, kStemSnapV) 351 | parseError = 1 352 | elif (kStdVW in instanceDict and kStemSnapV in instanceDict): # cannot be just 'else' because it will generate a 'KeyError' when these hinting parameters are not provided in the 'instances' file 353 | if instanceDict[kStemSnapV][0] != instanceDict[kStdVW][0]: 354 | print "ERROR: In line %s, the first value in %s must be the same as the %s value." % (i+1, kStemSnapV, kStdVW) 355 | parseError = 1 356 | 357 | instancesList.append(instanceDict) 358 | 359 | if parseError or len(instancesList) == 0: 360 | raise(ParseError) 361 | 362 | return instancesList 363 | 364 | 365 | def handleInstanceLight(f, fontInstanceDict, instanceInfo): 366 | # Set names 367 | f.font_name = fontInstanceDict[kFontName] 368 | 369 | instValues = fontInstanceDict[kCoordsKey] 370 | 371 | # This name does not go into the CFF font header. It's used in the 'features.kern' to have a record of the instance. 372 | # Builds information about the source font and instance values 373 | for x in range(len(instValues)): 374 | instanceInfo += '_' + str(instValues[x]) 375 | f.menu_name = instanceInfo 376 | 377 | return f 378 | 379 | 380 | def makeFaceFolder(root, folder): 381 | facePath = os.path.join(root, folder) 382 | if not os.path.exists(facePath): 383 | os.makedirs(facePath) 384 | return facePath 385 | 386 | 387 | def handleFontLight(folderPath, fontMM, fontInstanceDict, options): 388 | try: 389 | faceName = fontInstanceDict[kFontName].split('-')[1] 390 | except IndexError: 391 | faceName = 'Regular' 392 | 393 | print 394 | print faceName 395 | 396 | fontName = fontInstanceDict[kFontName] 397 | instValues = fontInstanceDict[kCoordsKey] 398 | 399 | try: 400 | fontInstance = Font(fontMM, instValues) # creates instance 401 | except: 402 | print "Error: Could not create instance <%s> (%s)" % (instValues, fontName) 403 | return 404 | 405 | instanceInfo = os.path.basename(fontMM.file_name) # The name of the source MM VFB is recorded as part of the info regarding the instance 406 | fontInstance = handleInstanceLight(fontInstance, fontInstanceDict, instanceInfo) 407 | 408 | instanceFolder = makeFaceFolder(folderPath, faceName) 409 | 410 | WriteFeaturesMarkFDK.MarkDataClass(fontInstance, instanceFolder, options.trimCasingTags, options.genMkmkFeature, options.writeClassesFile, options.indianScriptsFormat) 411 | 412 | 413 | def makeFeature(options): 414 | try: 415 | parentDir = os.path.dirname(os.path.abspath(fl.font.file_name)) 416 | except AttributeError: 417 | print "The font has not been saved. Please save the font and try again." 418 | return 419 | 420 | if fl.font[0].layers_number == 1: 421 | fontSM = fl.font # Single Master Font 422 | else: 423 | fontMM = fl.font # MM Font 424 | axisNum = int(math.log(fontMM[0].layers_number, 2)) # Number of axis in font 425 | 426 | instancesFilePath = os.path.join(parentDir, kInstancesDataFileName) 427 | 428 | if not os.path.isfile(instancesFilePath): 429 | print "Could not find the file named '%s' in the path below\n\t%s" % (kInstancesDataFileName, parentDir) 430 | return 431 | 432 | try: 433 | print "Parsing instances file..." 434 | instancesList = readInstanceFile(instancesFilePath) 435 | except ParseError: 436 | print "Error parsing file or file is empty." 437 | return 438 | 439 | # A few checks before proceeding... 440 | if instancesList: 441 | # Make sure that the instance values is compatible with the number of axis in the MM font 442 | for i in range(len(instancesList)): 443 | instanceDict = instancesList[i] 444 | axisVal = instanceDict[kCoordsKey] # Get AxisValues strings 445 | if axisNum != len(axisVal): 446 | print 'ERROR: The %s value for the instance named %s in the %s file is not compatible with the number of axis in the MM source font.' % (kCoordsKey, instanceDict[kFontName], kInstancesDataFileName) 447 | return 448 | 449 | folderPath = fl.GetPathName("Select parent directory to output file(s)") 450 | 451 | # Cancel was clicked or Esc key was pressed 452 | if not folderPath: 453 | return 454 | 455 | t1 = time.time() # Initiates a timer of the whole process 456 | 457 | if fl.font[0].layers_number == 1: 458 | print fontSM.font_name 459 | WriteFeaturesMarkFDK.MarkDataClass(fontSM, folderPath, options.trimCasingTags, options.genMkmkFeature, options.writeClassesFile, options.indianScriptsFormat) 460 | else: 461 | for fontInstance in instancesList: 462 | handleFontLight(folderPath, fontMM, fontInstance, options) 463 | 464 | t2 = time.time() 465 | elapsedSeconds = t2-t1 466 | 467 | if (elapsedSeconds/60) < 1: 468 | print '\nCompleted in %.1f seconds.\n' % elapsedSeconds 469 | else: 470 | print '\nCompleted in %.1f minutes.\n' % (elapsedSeconds/60) 471 | 472 | 473 | class MarkGenOptions: 474 | # Holds the options for the module. 475 | # The values of all member items NOT prefixed with "_" are written to/read from 476 | # a preferences file. 477 | # This also gets/sets the same member fields in the passed object. 478 | def __init__(self): 479 | self.trimCasingTags = 0 480 | self.genMkmkFeature = 0 481 | self.writeClassesFile = 0 482 | self.indianScriptsFormat = 0 483 | 484 | # items not written to prefs 485 | self._prefsBaseName = kPrefsFileName 486 | self._prefsPath = None 487 | 488 | def _getPrefs(self, callerObject = None): 489 | foundPrefsFile = 0 490 | 491 | # We will put the prefs file in a directory "Preferences" at the same level as the Macros directory 492 | dirPath = os.path.dirname(WriteFeaturesMarkFDK.__file__) 493 | name = " " 494 | while name and (name.lower() != "macros"): 495 | name = os.path.basename(dirPath) 496 | dirPath = os.path.dirname(dirPath) 497 | if name.lower() != "macros" : 498 | dirPath = None 499 | 500 | if dirPath: 501 | dirPath = os.path.join(dirPath, "Preferences") 502 | if not os.path.exists(dirPath): # create it so we can save a prefs file there later. 503 | try: 504 | os.mkdir(dirPath) 505 | except (IOError,OSError): 506 | print("Failed to create prefs directory %s" % (dirPath)) 507 | return foundPrefsFile 508 | else: 509 | return foundPrefsFile 510 | 511 | # the prefs directory exists. Try and open the file. 512 | self._prefsPath = os.path.join(dirPath, self._prefsBaseName) 513 | if os.path.exists(self._prefsPath): 514 | try: 515 | pf = file(self._prefsPath, "rt") 516 | data = pf.read() 517 | prefs = eval(data) 518 | pf.close() 519 | except (IOError, OSError): 520 | print("Prefs file exists but cannot be read %s" % (self._prefsPath)) 521 | return foundPrefsFile 522 | 523 | # We've successfully read the prefs file 524 | foundPrefsFile = 1 525 | kelList = prefs.keys() 526 | for key in kelList: 527 | exec("self.%s = prefs[\"%s\"]" % (key,key)) 528 | 529 | # Add/set the member fields of the calling object 530 | if callerObject: 531 | keyList = dir(self) 532 | for key in keyList: 533 | if key[0] == "_": 534 | continue 535 | exec("callerObject.%s = self.%s" % (key, key)) 536 | 537 | return foundPrefsFile 538 | 539 | 540 | def _savePrefs(self, callerObject = None): 541 | prefs = {} 542 | if not self._prefsPath: 543 | return 544 | 545 | keyList = dir(self) 546 | for key in keyList: 547 | if key[0] == "_": 548 | continue 549 | if callerObject: 550 | exec("self.%s = callerObject.%s" % (key, key)) 551 | exec("prefs[\"%s\"] = self.%s" % (key, key)) 552 | try: 553 | pf = file(self._prefsPath, "wt") 554 | pf.write(repr(prefs)) 555 | pf.close() 556 | print("Saved prefs in %s." % self._prefsPath) 557 | except (IOError, OSError): 558 | print("Failed to write prefs file in %s." % self._prefsPath) 559 | 560 | 561 | class MarkGenDialog: 562 | def __init__(self): 563 | """ NOTE: the Get and Save preferences class methods access the preference values as fields 564 | of the dialog by name. If you want to change a preference value, the dialog control value must have 565 | the same field name. 566 | """ 567 | dWidth = 350 568 | dMargin = 25 569 | xMax = dWidth - dMargin 570 | 571 | # Mark Feature Options section 572 | xC1 = dMargin + 20 # Left indent of the "Generate..." options 573 | yC0 = 0 574 | yC1 = yC0 + 30 575 | yC2 = yC1 + 30 576 | yC3 = yC2 + 30 577 | yC4 = yC3 + 30 578 | endYsection = yC4 + 30 579 | 580 | dHeight = endYsection + 70 # Total height of dialog 581 | 582 | self.d = Dialog(self) 583 | self.d.size = Point(dWidth, dHeight) 584 | self.d.Center() 585 | self.d.title = "Mark Feature Generator Preferences" 586 | 587 | self.options = MarkGenOptions() 588 | self.options._getPrefs(self) # This both loads prefs and assigns the member fields of the dialog. 589 | 590 | self.d.AddControl(CHECKBOXCONTROL, Rect(xC1, yC1, xMax, aAUTO), "genMkmkFeature", STYLE_CHECKBOX, " Write mark-to-mark lookups") 591 | self.d.AddControl(CHECKBOXCONTROL, Rect(xC1, yC2, xMax, aAUTO), "trimCasingTags", STYLE_CHECKBOX, " Trim casing tags on anchor names") 592 | self.d.AddControl(CHECKBOXCONTROL, Rect(xC1, yC3, xMax, aAUTO), "writeClassesFile", STYLE_CHECKBOX, " Write mark classes in separate file") 593 | self.d.AddControl(CHECKBOXCONTROL, Rect(xC1, yC4, xMax, aAUTO), "indianScriptsFormat", STYLE_CHECKBOX, " Format the output for Indian scripts") 594 | 595 | def on_genMkmkFeature(self, code): 596 | self.d.GetValue("genMkmkFeature") 597 | 598 | def on_trimCasingTags(self, code): 599 | self.d.GetValue("trimCasingTags") 600 | 601 | def on_writeClassesFile(self, code): 602 | self.d.GetValue("writeClassesFile") 603 | 604 | def on_indianScriptsFormat(self, code): 605 | self.d.GetValue("indianScriptsFormat") 606 | 607 | def on_ok(self,code): 608 | self.result = 1 609 | # update options 610 | self.options._savePrefs(self) # update prefs file 611 | 612 | def on_cancel(self, code): 613 | self.result = 0 614 | 615 | def Run(self): 616 | self.d.Run() 617 | return self.result 618 | 619 | 620 | def run(): 621 | global debug 622 | if fl.count == 0: 623 | print 'No font opened.' 624 | return 625 | 626 | if len(fl.font) == 0: 627 | print 'The font has no glyphs.' 628 | return 629 | 630 | else: 631 | dontShowDialog = 1 632 | result = 2 633 | dontShowDialog = checkControlKeyPress() 634 | debug = not checkShiftKeyPress() 635 | if dontShowDialog: 636 | print "Hold down CONTROL key while starting this script in order to set options.\n" 637 | options = MarkGenOptions() 638 | options._getPrefs() # load current settings from prefs 639 | makeFeature(options) 640 | else: 641 | IGd = MarkGenDialog() 642 | result = IGd.Run() # returns 0 for cancel, 1 for ok 643 | if result == 1: 644 | options = MarkGenOptions() 645 | options._getPrefs() # load current settings from prefs 646 | makeFeature(options) 647 | 648 | 649 | if __name__ == "__main__": 650 | run() 651 | -------------------------------------------------------------------------------- /Anchors/RemoveAnchorsFromSelectedGlyphs.py: -------------------------------------------------------------------------------- 1 | #FLM: Remove Anchors From Selected Glyphs 2 | 3 | __copyright__ = __license__ = """ 4 | Copyright (c) 2010-2013 Adobe Systems Incorporated. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | """ 23 | 24 | __doc__ = """ 25 | Remove Anchors From Selected Glyphs v1.2 - Apr 10 2013 26 | 27 | Removes the anchors on the selected glyphs. 28 | 29 | ================================================== 30 | Versions: 31 | v1.0 - Apr 29 2010 - Initial version. 32 | v1.1 - Jun 15 2012 - Added undo support. 33 | v1.2 - Apr 10 2013 - Remaned the macro from "Remove Anchors" to the current name. 34 | 35 | """ 36 | 37 | listGlyphsSelected = [] 38 | def getgselectedglyphs(font, glyph, gindex): 39 | listGlyphsSelected.append(gindex) 40 | fl.ForSelected(getgselectedglyphs) 41 | 42 | anchorFound = False 43 | 44 | for gIndex in listGlyphsSelected: 45 | glyph = fl.font[gIndex] 46 | if len(glyph.anchors) > 0: 47 | fl.SetUndo(gIndex) 48 | anchorIndexList = range(len(glyph.anchors)) 49 | anchorIndexList.reverse() # reversed() was added in Python 2.4; FL 5.0.4 is tied to Python 2.3 50 | for i in anchorIndexList: # delete from last to first 51 | del glyph.anchors[i] 52 | fl.UpdateGlyph(gIndex) 53 | anchorFound = True 54 | 55 | if anchorFound: 56 | fl.font.modified = 1 57 | print 'Anchors removed!' 58 | else: 59 | print 'No anchors found!' 60 | -------------------------------------------------------------------------------- /Hinting/AutoHint.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe-type-tools/fontlab-scripts/8f6a2009fc7d8be2b0bf6f99640297f742923a00/Hinting/AutoHint.py -------------------------------------------------------------------------------- /Kerning/ExportClassKerningToUFO.py: -------------------------------------------------------------------------------- 1 | #FLM: Export FontLab class kerning to UFO 2 | 3 | import os 4 | from FL import * 5 | 6 | module_found = False 7 | defcon_found = False 8 | 9 | modules_urls = { 10 | 'defcon': 'https://github.com/typesupply/defcon', 11 | 'ufoLib': 'https://github.com/unified-font-object/ufoLib', 12 | 'fontTools.misc.py23': 'https://github.com/fonttools/fonttools', 13 | 'kernExport': 'https://github.com/adobe-type-tools/python-modules', 14 | } 15 | 16 | 17 | def print_module_msg(err): 18 | module_name = err.message.split()[-1] 19 | print('%s was found.' % err) 20 | print('Get it at %s' % modules_urls.get(module_name)) 21 | 22 | 23 | try: 24 | import defcon 25 | defcon_found = True 26 | 27 | except ImportError as err: 28 | print_module_msg(err) 29 | print('') 30 | 31 | 32 | if defcon_found: 33 | try: 34 | import kernExport 35 | module_found = True 36 | 37 | except ImportError as err: 38 | print_module_msg(err) 39 | module_path = os.path.join(fl.userpath, 'Macros', 'System', 'Modules') 40 | print("Then place kernExport.py file in FontLab's Modules folder at") 41 | print("%s" % module_path) 42 | print('') 43 | 44 | 45 | def run(): 46 | if module_found: 47 | 48 | # Three options are possible for kerning classes prefix: 49 | # 'MM': MetricsMachine-style 50 | # 'UFO3': UFO3-style 51 | # None: don't change the name of kerning classes (apart from 52 | # the side markers) 53 | # For further details see kernExport.__doc__ 54 | 55 | kernExport.ClassKerningToUFO(fl.font, prefixOption='MM') 56 | 57 | 58 | if __name__ == '__main__': 59 | run() 60 | -------------------------------------------------------------------------------- /Kerning/KernFeatureGenerator.py: -------------------------------------------------------------------------------- 1 | #FLM: Kern Feature Generator 2 | 3 | ################################################### 4 | ### THE VALUES BELOW CAN BE EDITED AS NEEDED ###### 5 | ################################################### 6 | 7 | kInstancesDataFileName = "instances" 8 | kPrefsFileName = "KernFeatureGenerator.prefs" 9 | 10 | ################################################### 11 | 12 | __copyright__ = __license__ = """ 13 | Copyright (c) 2006-2013 Adobe Systems Incorporated. All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a 16 | copy of this software and associated documentation files (the "Software"), 17 | to deal in the Software without restriction, including without limitation 18 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 19 | and/or sell copies of the Software, and to permit persons to whom the 20 | Software is furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 31 | DEALINGS IN THE SOFTWARE. 32 | """ 33 | 34 | __doc__ = """ 35 | Kern Feature Generator v2.2.1 - Mar 10 2013 36 | 37 | This script will generate a set of "features.kern" files from the kerning data (kerning 38 | pairs and kerning classes) of a Multiple Master (MM) FontLab font, or one "features.kern" 39 | file if the font is Single Master (SM). The "features.kern" file is a text file containing 40 | the font's kerning data in features-file definition syntax. 41 | 42 | To access the script's options, hold down the CONTROL key while clicking on the play 43 | button to run the script. 44 | 45 | The script will first show a file selection dialog for choosing a folder. The "features.kern" 46 | file(s) will be written to that folder, if the font is SM, or to a sub-directory path 47 | /, if the font is MM. In this case, the face name is derived by 48 | taking the part of the font's PostScript name after the hyphen, or "Regular" is there is no 49 | hyphen. (e.g. if the font's PostScript name is MyFont-BoldItalic, the folder will be named 50 | "BoldItalic") 51 | 52 | If the font is MM, the script requires a file, named "instances", which contains all the 53 | instance-specific values. The "instances" file must be a simple text file, located in the 54 | same folder as the MM FontLab file. 55 | 56 | For information on how to format the "instances" file, please read the documentation in the 57 | InstanceGenerator.py script. 58 | 59 | For information about how the "features.kern" file is created, please read the documentation in 60 | the WriteFeaturesKernFDK.py module that can be found in FontLab/Studio5/Macros/System/Modules/ 61 | 62 | ================================================== 63 | Versions: 64 | v1.0 - Apr 24 2007 - Initial release 65 | v1.1 - Dec 03 2007 - Robofab dependency removed (code changes suggested by Karsten Luecke) 66 | v2.0 - Feb 15 2010 - Complete rewrite to align with the changes made to InstanceGenerator.py 67 | and WriteFeaturesKernFDK.py 68 | When processing a MM font, each font instance is no longer displayed. 69 | v2.1 - Feb 19 2010 - Added checks to verify the existence of kern pairs and kern classes. 70 | Improved the dialog window. 71 | v2.2 - Jan 24 2013 - Added subtable-option to dialog window. 72 | v2.2.1 - Mar 10 2013 - Minor improvements. 73 | 74 | """ 75 | 76 | import os, sys, re, copy, math, time 77 | 78 | try: 79 | from AdobeFontLabUtils import checkControlKeyPress, checkShiftKeyPress 80 | import WriteFeaturesKernFDK 81 | # reload(WriteFeaturesKernFDK) 82 | except ImportError,e: 83 | print "Failed to find the Adobe FDK support scripts." 84 | print "Please run the script FDK/Tools/FontLab/installFontLabMacros.py script, and try again." 85 | print "Current directory: ", os.path.abspath(os.getcwd()) 86 | print "Current list of search paths for modules: " 87 | import pprint 88 | pprint.pprint(sys.path) 89 | raise e 90 | 91 | kFieldsKey = "#KEYS:" 92 | kFamilyName = "FamilyName" 93 | kFontName = "FontName" 94 | kFullName = "FullName" 95 | kWeight = "Weight" 96 | kCoordsKey = "Coords" 97 | kIsBoldKey = "IsBold" # This is changed to kForceBold in the instanceDict when reading in the instance file. 98 | kForceBold = "ForceBold" 99 | kIsItalicKey = "IsItalic" 100 | kExceptionSuffixes = "ExceptionSuffixes" 101 | kExtraGlyphs = "ExtraGlyphs" 102 | 103 | kFixedFieldKeys = { 104 | # field index: key name 105 | 0:kFamilyName, 106 | 1:kFontName, 107 | 2:kFullName, 108 | 3:kWeight, 109 | 4:kCoordsKey, 110 | 5:kIsBoldKey, 111 | } 112 | 113 | kNumFixedFields = len(kFixedFieldKeys) 114 | 115 | kBlueScale = "BlueScale" 116 | kBlueShift = "BlueShift" 117 | kBlueFuzz = "BlueFuzz" 118 | kBlueValues = "BlueValues" 119 | kOtherBlues = "OtherBlues" 120 | kFamilyBlues = "FamilyBlues" 121 | kFamilyOtherBlues = "FamilyOtherBlues" 122 | kStdHW = "StdHW" 123 | kStdVW = "StdVW" 124 | kStemSnapH = "StemSnapH" 125 | kStemSnapV = "StemSnapV" 126 | 127 | kAlignmentZonesKeys = [kBlueValues, kOtherBlues, kFamilyBlues, kFamilyOtherBlues] 128 | kTopAlignZonesKeys = [kBlueValues, kFamilyBlues] 129 | kMaxTopZonesSize = 14 # 7 zones 130 | kBotAlignZonesKeys = [kOtherBlues, kFamilyOtherBlues] 131 | kMaxBotZonesSize = 10 # 5 zones 132 | kStdStemsKeys = [kStdHW, kStdVW] 133 | kMaxStdStemsSize = 1 134 | kStemSnapKeys = [kStemSnapH, kStemSnapV] 135 | kMaxStemSnapSize = 12 # including StdStem 136 | 137 | 138 | class ParseError(ValueError): 139 | pass 140 | 141 | 142 | def validateArrayValues(arrayList, valuesMustBePositive): 143 | for i in range(len(arrayList)): 144 | try: 145 | arrayList[i] = eval(arrayList[i]) 146 | except (NameError, SyntaxError): 147 | return 148 | if valuesMustBePositive: 149 | if arrayList[i] < 0: 150 | return 151 | return arrayList 152 | 153 | 154 | def readInstanceFile(instancesFilePath): 155 | f = open(instancesFilePath, "rt") 156 | data = f.read() 157 | f.close() 158 | 159 | lines = data.splitlines() 160 | 161 | i = 0 162 | parseError = 0 163 | keyDict = copy.copy(kFixedFieldKeys) 164 | numKeys = kNumFixedFields 165 | numLines = len(lines) 166 | instancesList = [] 167 | 168 | for i in range(numLines): 169 | line = lines[i] 170 | 171 | # Skip over blank lines 172 | line2 = line.strip() 173 | if not line2: 174 | continue 175 | 176 | # Get rid of all comments. If we find a key definition comment line, parse it. 177 | commentIndex = line.find('#') 178 | if commentIndex >= 0: 179 | if line.startswith(kFieldsKey): 180 | if instancesList: 181 | print "ERROR: Header line (%s) must preceed a data line." % kFieldsKey 182 | raise ParseError 183 | # parse the line with the field names. 184 | line = line[len(kFieldsKey):] 185 | line = line.strip() 186 | keys = line.split('\t') 187 | keys = map(lambda name: name.strip(), keys) 188 | numKeys = len(keys) 189 | k = kNumFixedFields 190 | while k < numKeys: 191 | keyDict[k] = keys[k] 192 | k +=1 193 | continue 194 | else: 195 | line = line[:commentIndex] 196 | continue 197 | 198 | # Must be a data line. 199 | fields = line.split('\t') 200 | fields = map(lambda datum: datum.strip(), fields) 201 | numFields = len(fields) 202 | if (numFields != numKeys): 203 | print "ERROR: In line %s, the number of fields %s does not match the number of key names %s (FamilyName, FontName, FullName, Weight, Coords, IsBold)." % (i+1, numFields, numKeys) 204 | parseError = 1 205 | continue 206 | 207 | instanceDict= {} 208 | #Build a dict from key to value. Some kinds of values needs special processing. 209 | for k in range(numFields): 210 | key = keyDict[k] 211 | field = fields[k] 212 | if not field: 213 | continue 214 | if field in ["Default", "None", "FontBBox"]: 215 | # FontBBox is no longer supported - I calculate the real 216 | # instance fontBBox from the glyph metrics instead, 217 | continue 218 | if key == kFontName: 219 | value = field 220 | elif key in [kExtraGlyphs, kExceptionSuffixes]: 221 | value = eval(field) 222 | elif key in [kIsBoldKey, kIsItalicKey, kCoordsKey]: 223 | try: 224 | value = eval(field) # this works for all three fields. 225 | 226 | if key == kIsBoldKey: # need to convert to Type 1 field key. 227 | instanceDict[key] = value 228 | # add kForceBold key. 229 | key = kForceBold 230 | if value == 1: 231 | value = "true" 232 | else: 233 | value = "false" 234 | elif key == kIsItalicKey: 235 | if value == 1: 236 | value = "true" 237 | else: 238 | value = "false" 239 | elif key == kCoordsKey: 240 | if type(value) == type(0): 241 | value = (value,) 242 | except (NameError, SyntaxError): 243 | print "ERROR: In line %s, the %s field has an invalid value." % (i+1, key) 244 | parseError = 1 245 | continue 246 | 247 | elif field[0] in ["[","{"]: # it is a Type 1 array value. Turn it into a list and verify that there's an even number of values for the alignment zones 248 | value = field[1:-1].split() # Remove the begin and end brackets/braces, and make a list 249 | 250 | if key in kAlignmentZonesKeys: 251 | if len(value) % 2 != 0: 252 | print "ERROR: In line %s, the %s field does not have an even number of values." % (i+1, key) 253 | parseError = 1 254 | continue 255 | 256 | if key in kTopAlignZonesKeys: # The Type 1 spec only allows 7 top zones (7 pairs of values) 257 | if len(value) > kMaxTopZonesSize: 258 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxTopZonesSize) 259 | parseError = 1 260 | continue 261 | else: 262 | newArray = validateArrayValues(value, False) # False = values do NOT have to be all positive 263 | if newArray: 264 | value = newArray 265 | else: 266 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 267 | parseError = 1 268 | continue 269 | currentArray = value[:] # make copy, not reference 270 | value.sort() 271 | if currentArray != value: 272 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 273 | 274 | if key in kBotAlignZonesKeys: # The Type 1 spec only allows 5 top zones (5 pairs of values) 275 | if len(value) > kMaxBotZonesSize: 276 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxBotZonesSize) 277 | parseError = 1 278 | continue 279 | else: 280 | newArray = validateArrayValues(value, False) # False = values do NOT have to be all positive 281 | if newArray: 282 | value = newArray 283 | else: 284 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 285 | parseError = 1 286 | continue 287 | currentArray = value[:] # make copy, not reference 288 | value.sort() 289 | if currentArray != value: 290 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 291 | 292 | if key in kStdStemsKeys: 293 | if len(value) > kMaxStdStemsSize: 294 | print "ERROR: In line %s, the %s field can only have %d value." % (i+1, key, kMaxStdStemsSize) 295 | parseError = 1 296 | continue 297 | else: 298 | newArray = validateArrayValues(value, True) # True = all values must be positive 299 | if newArray: 300 | value = newArray 301 | else: 302 | print "ERROR: In line %s, the %s field has an invalid value." % (i+1, key) 303 | parseError = 1 304 | continue 305 | 306 | if key in kStemSnapKeys: # The Type 1 spec only allows 12 stem widths, including 1 standard stem 307 | if len(value) > kMaxStemSnapSize: 308 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxStemSnapSize) 309 | parseError = 1 310 | continue 311 | else: 312 | newArray = validateArrayValues(value, True) # True = all values must be positive 313 | if newArray: 314 | value = newArray 315 | else: 316 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 317 | parseError = 1 318 | continue 319 | currentArray = value[:] # make copy, not reference 320 | value.sort() 321 | if currentArray != value: 322 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 323 | else: 324 | # either a single number or a string. 325 | if re.match(r"^[-.\d]+$", field): 326 | value = field #it is a Type 1 number. Pass as is, as a string. 327 | else: 328 | value = field 329 | 330 | instanceDict[key] = value 331 | 332 | if (kStdHW in instanceDict and kStemSnapH not in instanceDict) or (kStdHW not in instanceDict and kStemSnapH in instanceDict): 333 | print "ERROR: In line %s, either the %s value or the %s values are missing or were invalid." % (i+1, kStdHW, kStemSnapH) 334 | parseError = 1 335 | elif (kStdHW in instanceDict and kStemSnapH in instanceDict): # cannot be just 'else' because it will generate a 'KeyError' when these hinting parameters are not provided in the 'instances' file 336 | if instanceDict[kStemSnapH][0] != instanceDict[kStdHW][0]: 337 | print "ERROR: In line %s, the first value in %s must be the same as the %s value." % (i+1, kStemSnapH, kStdHW) 338 | parseError = 1 339 | 340 | if (kStdVW in instanceDict and kStemSnapV not in instanceDict) or (kStdVW not in instanceDict and kStemSnapV in instanceDict): 341 | print "ERROR: In line %s, either the %s value or the %s values are missing or were invalid." % (i+1, kStdVW, kStemSnapV) 342 | parseError = 1 343 | elif (kStdVW in instanceDict and kStemSnapV in instanceDict): # cannot be just 'else' because it will generate a 'KeyError' when these hinting parameters are not provided in the 'instances' file 344 | if instanceDict[kStemSnapV][0] != instanceDict[kStdVW][0]: 345 | print "ERROR: In line %s, the first value in %s must be the same as the %s value." % (i+1, kStemSnapV, kStdVW) 346 | parseError = 1 347 | 348 | instancesList.append(instanceDict) 349 | 350 | if parseError or len(instancesList) == 0: 351 | raise(ParseError) 352 | 353 | return instancesList 354 | 355 | 356 | def handleInstanceLight(f, fontInstanceDict, instanceInfo): 357 | # Set names 358 | f.font_name = fontInstanceDict[kFontName] 359 | 360 | instValues = fontInstanceDict[kCoordsKey] 361 | 362 | # This name does not go into the CFF font header. It's used in the 'features.kern' to have a record of the instance. 363 | # Builds information about the source font and instance values 364 | for x in range(len(instValues)): 365 | instanceInfo += '_' + str(instValues[x]) 366 | f.menu_name = instanceInfo 367 | 368 | return f 369 | 370 | 371 | def makeFaceFolder(root, folder): 372 | facePath = os.path.join(root, folder) 373 | if not os.path.exists(facePath): 374 | os.makedirs(facePath) 375 | return facePath 376 | 377 | 378 | def handleFontLight(folderPath, fontMM, fontInstanceDict, options): 379 | try: 380 | faceName = fontInstanceDict[kFontName].split('-')[1] 381 | except IndexError: 382 | faceName = 'Regular' 383 | 384 | print 385 | print faceName 386 | 387 | fontName = fontInstanceDict[kFontName] 388 | instValues = fontInstanceDict[kCoordsKey] 389 | 390 | try: 391 | fontInstance = Font(fontMM, instValues) # creates instance 392 | except: 393 | print "Error: Could not create instance <%s> (%s)" % (instValues, fontName) 394 | return 395 | 396 | instanceInfo = os.path.basename(fontMM.file_name) # The name of the source MM VFB is recorded as part of the info regarding the instance 397 | fontInstance = handleInstanceLight(fontInstance, fontInstanceDict, instanceInfo) 398 | 399 | instanceFolder = makeFaceFolder(folderPath, faceName) 400 | 401 | WriteFeaturesKernFDK.KernDataClass(fontInstance, instanceFolder, options.minKern, options.writeTrimmed, options.writeSubtables) 402 | 403 | 404 | def makeFeature(options): 405 | try: 406 | parentDir = os.path.dirname(os.path.abspath(fl.font.file_name)) 407 | except AttributeError: 408 | print "The font has not been saved. Please save the font and try again." 409 | return 410 | 411 | if fl.font[0].layers_number == 1: 412 | fontSM = fl.font # Single Master Font 413 | else: 414 | fontMM = fl.font # MM Font 415 | axisNum = int(math.log(fontMM[0].layers_number, 2)) # Number of axis in font 416 | 417 | instancesFilePath = os.path.join(parentDir, kInstancesDataFileName) 418 | 419 | if not os.path.isfile(instancesFilePath): 420 | print "Could not find the file named '%s' in the path below\n\t%s" % (kInstancesDataFileName, parentDir) 421 | return 422 | 423 | try: 424 | print "Parsing instances file..." 425 | instancesList = readInstanceFile(instancesFilePath) 426 | except ParseError: 427 | print "Error parsing file or file is empty." 428 | return 429 | 430 | # A few checks before proceeding... 431 | if instancesList: 432 | # Make sure that the instance values is compatible with the number of axis in the MM font 433 | for i in range(len(instancesList)): 434 | instanceDict = instancesList[i] 435 | axisVal = instanceDict[kCoordsKey] # Get AxisValues strings 436 | if axisNum != len(axisVal): 437 | print 'ERROR: The %s value for the instance named %s in the %s file is not compatible with the number of axis in the MM source font.' % (kCoordsKey, instanceDict[kFontName], kInstancesDataFileName) 438 | return 439 | 440 | folderPath = fl.GetPathName("Select parent directory to output file(s)") 441 | 442 | # Cancel was clicked or Esc key was pressed 443 | if not folderPath: 444 | return 445 | 446 | t1 = time.time() # Initiates a timer of the whole process 447 | 448 | if fl.font[0].layers_number == 1: 449 | print fontSM.font_name 450 | WriteFeaturesKernFDK.KernDataClass(fontSM, folderPath, options.minKern, options.writeTrimmed, options.writeSubtables) 451 | else: 452 | for fontInstance in instancesList: 453 | handleFontLight(folderPath, fontMM, fontInstance, options) 454 | 455 | t2 = time.time() 456 | elapsedSeconds = t2-t1 457 | clock = time.strftime('%H:%M:%S', time.localtime()) 458 | 459 | if (elapsedSeconds/60) < 1: 460 | print '\nCompleted in %.1f seconds at %s.\n' % (elapsedSeconds, clock) 461 | else: 462 | print '\nCompleted in %.1f minutes at %s.\n' % (elapsedSeconds/60, clock) 463 | 464 | 465 | class KernGenOptions: 466 | # Holds the options for the module. 467 | # The values of all member items NOT prefixed with "_" are written to/read from 468 | # a preferences file. 469 | # This also gets/sets the same member fields in the passed object. 470 | def __init__(self): 471 | self.minKern = 3 472 | self.writeTrimmed = 0 473 | self.writeSubtables = 1 474 | 475 | # items not written to prefs 476 | self._prefsBaseName = kPrefsFileName 477 | self._prefsPath = None 478 | 479 | def _getPrefs(self, callerObject = None): 480 | foundPrefsFile = 0 481 | 482 | # We will put the prefs file in a directory "Preferences" at the same level as the Macros directory 483 | dirPath = os.path.dirname(WriteFeaturesKernFDK.__file__) 484 | name = " " 485 | while name and (name.lower() != "macros"): 486 | name = os.path.basename(dirPath) 487 | dirPath = os.path.dirname(dirPath) 488 | if name.lower() != "macros" : 489 | dirPath = None 490 | 491 | if dirPath: 492 | dirPath = os.path.join(dirPath, "Preferences") 493 | if not os.path.exists(dirPath): # create it so we can save a prefs file there later. 494 | try: 495 | os.mkdir(dirPath) 496 | except (IOError,OSError): 497 | print("Failed to create prefs directory %s" % (dirPath)) 498 | return foundPrefsFile 499 | else: 500 | return foundPrefsFile 501 | 502 | # the prefs directory exists. Try and open the file. 503 | self._prefsPath = os.path.join(dirPath, self._prefsBaseName) 504 | if os.path.exists(self._prefsPath): 505 | try: 506 | pf = file(self._prefsPath, "rt") 507 | data = pf.read() 508 | prefs = eval(data) 509 | pf.close() 510 | except (IOError, OSError): 511 | print("Prefs file exists but cannot be read %s" % (self._prefsPath)) 512 | return foundPrefsFile 513 | 514 | # We've successfully read the prefs file 515 | foundPrefsFile = 1 516 | kelList = prefs.keys() 517 | for key in kelList: 518 | exec("self.%s = prefs[\"%s\"]" % (key,key)) 519 | 520 | # Add/set the member fields of the calling object 521 | if callerObject: 522 | keyList = dir(self) 523 | for key in keyList: 524 | if key[0] == "_": 525 | continue 526 | exec("callerObject.%s = self.%s" % (key, key)) 527 | 528 | return foundPrefsFile 529 | 530 | 531 | def _savePrefs(self, callerObject = None): 532 | prefs = {} 533 | if not self._prefsPath: 534 | return 535 | 536 | keyList = dir(self) 537 | for key in keyList: 538 | if key[0] == "_": 539 | continue 540 | if callerObject: 541 | exec("self.%s = callerObject.%s" % (key, key)) 542 | exec("prefs[\"%s\"] = self.%s" % (key, key)) 543 | try: 544 | pf = file(self._prefsPath, "wt") 545 | pf.write(repr(prefs)) 546 | pf.close() 547 | print("Saved prefs in %s." % self._prefsPath) 548 | except (IOError, OSError): 549 | print("Failed to write prefs file in %s." % self._prefsPath) 550 | 551 | 552 | class KernGenDialog: 553 | def __init__(self): 554 | """ NOTE: the Get and Save preferences class methods access the preference values as fields 555 | of the dialog by name. If you want to change a preference value, the dialog control value must have 556 | the same field name. 557 | """ 558 | dWidth = 350 559 | dMargin = 25 560 | xMax = dWidth - dMargin 561 | 562 | # Kern Feature Options section 563 | xA1 = dMargin + 20 # Left indent of the "Generate..." options 564 | xB0 = xA1 - 5 565 | xB1 = xA1 + 20 # Left indent of "Minimum kerning" text 566 | xB2 = xA1 567 | yB0 = 0 568 | yB1 = yB0 + 30 569 | yB2 = yB1 + 30 570 | yB3 = yB2 + 30 571 | endYsection2 = yB3 + 30 572 | 573 | dHeight = endYsection2 + 70 # Total height of dialog 574 | 575 | self.d = Dialog(self) 576 | self.d.size = Point(dWidth, dHeight) 577 | self.d.Center() 578 | self.d.title = "Kern Feature Generator Preferences" 579 | 580 | self.options = KernGenOptions() 581 | self.options._getPrefs(self) # This both loads prefs and assigns the member fields of the dialog. 582 | 583 | self.d.AddControl(EDITCONTROL, Rect(xB0, yB1-5, xB0+20, aAUTO), "minKern", STYLE_EDIT+cTO_CENTER) 584 | self.d.AddControl(STATICCONTROL, Rect(xB1, yB1, xMax, aAUTO), "legend", STYLE_LABEL, " Minimum kern value (inclusive)") 585 | self.d.AddControl(CHECKBOXCONTROL, Rect(xB2, yB2, xMax, aAUTO), "writeTrimmed", STYLE_CHECKBOX, " Write trimmed pairs") 586 | self.d.AddControl(CHECKBOXCONTROL, Rect(xB2, yB3, xMax, aAUTO), "writeSubtables", STYLE_CHECKBOX, " Write subtables") 587 | 588 | def on_minKern(self, code): 589 | self.d.GetValue("minKern") 590 | 591 | def on_writeTrimmed(self, code): 592 | self.d.GetValue("writeTrimmed") 593 | 594 | def on_writeSubtables(self, code): 595 | self.d.GetValue("writeSubtables") 596 | 597 | def on_ok(self,code): 598 | self.result = 1 599 | # update options 600 | self.options._savePrefs(self) # update prefs file 601 | 602 | def on_cancel(self, code): 603 | self.result = 0 604 | 605 | def Run(self): 606 | self.d.Run() 607 | return self.result 608 | 609 | 610 | def run(): 611 | global debug 612 | if fl.count == 0: 613 | print 'No font opened.' 614 | return 615 | 616 | if len(fl.font) == 0: 617 | print 'The font has no glyphs.' 618 | return 619 | 620 | else: 621 | dontShowDialog = 1 622 | result = 2 623 | dontShowDialog = checkControlKeyPress() 624 | debug = not checkShiftKeyPress() 625 | if dontShowDialog: 626 | print "Hold down CONTROL key while starting this script in order to set options.\n" 627 | options = KernGenOptions() 628 | options._getPrefs() # load current settings from prefs 629 | makeFeature(options) 630 | else: 631 | IGd = KernGenDialog() 632 | result = IGd.Run() # returns 0 for cancel, 1 for ok 633 | if result == 1: 634 | options = KernGenOptions() 635 | options._getPrefs() # load current settings from prefs 636 | makeFeature(options) 637 | 638 | 639 | if __name__ == "__main__": 640 | run() 641 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MM Designs/SaveFilesForMakeInstances.py: -------------------------------------------------------------------------------- 1 | #FLM: Save Files for MakeInstances 2 | 3 | ################################################### 4 | ### THE VALUES BELOW CAN BE EDITED AS NEEDED ###### 5 | ################################################### 6 | 7 | kDefaultMMFontFileName = "mmfont.pfa" 8 | kInstancesDataFileName = "instances" 9 | kCompositeDataName = "temp.composite.dat" 10 | 11 | ################################################### 12 | 13 | __copyright__ = """ 14 | Copyright 2014-2016 Adobe Systems Incorporated (http://www.adobe.com/). All Rights Reserved. 15 | This software is licensed as OpenSource, under the Apache License, Version 2.0. This license is available at: http://opensource.org/licenses/Apache-2.0. 16 | """ 17 | 18 | __doc__ = """ 19 | Save Files for MakeInstances v2.0 - April 12 2016 20 | 21 | This script will do part of the work to create a set of single-master fonts 22 | ("instances") from a Multiple Master (MM) FontLab font. It will save a 23 | Type 1 MM font (needed by the makeInstances program) and, in some cases, 24 | a text file named 'temp.composite.dat' that contains data related with 25 | composite glyphs. 26 | 27 | You must then run the makeInstances program to actually build the instance Type 1 28 | fonts. makeInstances can remove working glyphs, and rename MM-exception glyphs. 29 | It will also do overlap removal, and autohint the instance fonts. This last is 30 | desirable, as autohinting which is specific to an instance font is usually 31 | significantly better than the hinting from interpolating the MM font hints. 32 | As always with overlap removal, you should check all affected glyphs - it 33 | doesn't always do the right thing. 34 | 35 | Note that the makeInstances program can be run alone, given an MM Type1 font 36 | file. However, if you use the ExceptionSuffixes keyword, then you must run 37 | this script first. The script will make a file that identifies composite glyphs, 38 | and allows makeInstances to correctly substitute contours in the composite glyph 39 | from the exception glyph. This is necessary because FontLab cannot write all the 40 | composite glyphs as Type 1 composites (also known as SEAC glyphs). This script 41 | must be run again to renew this data file whenever changes are made to composite 42 | glyphs. 43 | 44 | Both this script and the "makeInstances" program depend on info provided by an 45 | external text file named "instances", which contains all the instance-specific 46 | values. The "instances" file must be a simple text file, located in the same 47 | folder as the MM FontLab file. 48 | 49 | For information on how to format the "instances" file, please read the 50 | documentation in the InstanceGenerator.py script. 51 | 52 | ================================================== 53 | 54 | Versions: 55 | v2.0 - Apr 12 2016 - Added step to fix the MM FontBBox values of the mmfont.pfa file, 56 | when the VFB's UPM value is not 1000 (long-standing FontLab bug). 57 | v1.0 - Feb 15 2010 - Initial release 58 | 59 | """ 60 | 61 | import copy 62 | import re 63 | import os 64 | 65 | kFieldsKey = "#KEYS:" 66 | kFamilyName = "FamilyName" 67 | kFontName = "FontName" 68 | kFullName = "FullName" 69 | kWeight = "Weight" 70 | kCoordsKey = "Coords" 71 | kIsBoldKey = "IsBold" # This is changed to kForceBold in the instanceDict when reading in the instance file. 72 | kForceBold = "ForceBold" 73 | kIsItalicKey = "IsItalic" 74 | kExceptionSuffixes = "ExceptionSuffixes" 75 | kExtraGlyphs = "ExtraGlyphs" 76 | 77 | kFixedFieldKeys = { 78 | # field index: key name 79 | 0:kFamilyName, 80 | 1:kFontName, 81 | 2:kFullName, 82 | 3:kWeight, 83 | 4:kCoordsKey, 84 | 5:kIsBoldKey, 85 | } 86 | 87 | kNumFixedFields = len(kFixedFieldKeys) 88 | 89 | kBlueScale = "BlueScale" 90 | kBlueShift = "BlueShift" 91 | kBlueFuzz = "BlueFuzz" 92 | kBlueValues = "BlueValues" 93 | kOtherBlues = "OtherBlues" 94 | kFamilyBlues = "FamilyBlues" 95 | kFamilyOtherBlues = "FamilyOtherBlues" 96 | kStdHW = "StdHW" 97 | kStdVW = "StdVW" 98 | kStemSnapH = "StemSnapH" 99 | kStemSnapV = "StemSnapV" 100 | 101 | kAlignmentZonesKeys = [kBlueValues, kOtherBlues, kFamilyBlues, kFamilyOtherBlues] 102 | kTopAlignZonesKeys = [kBlueValues, kFamilyBlues] 103 | kMaxTopZonesSize = 14 # 7 zones 104 | kBotAlignZonesKeys = [kOtherBlues, kFamilyOtherBlues] 105 | kMaxBotZonesSize = 10 # 5 zones 106 | kStdStemsKeys = [kStdHW, kStdVW] 107 | kMaxStdStemsSize = 1 108 | kStemSnapKeys = [kStemSnapH, kStemSnapV] 109 | kMaxStemSnapSize = 12 # including StdStem 110 | 111 | 112 | class ParseError(ValueError): 113 | pass 114 | 115 | 116 | def validateArrayValues(arrayList, valuesMustBePositive): 117 | for i in range(len(arrayList)): 118 | try: 119 | arrayList[i] = eval(arrayList[i]) 120 | except (NameError, SyntaxError): 121 | return 122 | if valuesMustBePositive: 123 | if arrayList[i] < 0: 124 | return 125 | return arrayList 126 | 127 | 128 | def readInstanceFile(instancesFilePath): 129 | f = open(instancesFilePath, "rt") 130 | data = f.read() 131 | f.close() 132 | 133 | lines = data.splitlines() 134 | 135 | i = 0 136 | parseError = 0 137 | keyDict = copy.copy(kFixedFieldKeys) 138 | numKeys = kNumFixedFields 139 | numLines = len(lines) 140 | instancesList = [] 141 | 142 | for i in range(numLines): 143 | line = lines[i] 144 | 145 | # Skip over blank lines 146 | line2 = line.strip() 147 | if not line2: 148 | continue 149 | 150 | # Get rid of all comments. If we find a key definition comment line, parse it. 151 | commentIndex = line.find('#') 152 | if commentIndex >= 0: 153 | if line.startswith(kFieldsKey): 154 | if instancesList: 155 | print "ERROR: Header line (%s) must preceed a data line." % kFieldsKey 156 | raise ParseError 157 | # parse the line with the field names. 158 | line = line[len(kFieldsKey):] 159 | line = line.strip() 160 | keys = line.split('\t') 161 | keys = map(lambda name: name.strip(), keys) 162 | numKeys = len(keys) 163 | k = kNumFixedFields 164 | while k < numKeys: 165 | keyDict[k] = keys[k] 166 | k +=1 167 | continue 168 | else: 169 | line = line[:commentIndex] 170 | continue 171 | 172 | # Must be a data line. 173 | fields = line.split('\t') 174 | fields = map(lambda datum: datum.strip(), fields) 175 | numFields = len(fields) 176 | if (numFields != numKeys): 177 | print "ERROR: In line %s, the number of fields %s does not match the number of key names %s (FamilyName, FontName, FullName, Weight, Coords, IsBold)." % (i+1, numFields, numKeys) 178 | parseError = 1 179 | continue 180 | 181 | instanceDict= {} 182 | #Build a dict from key to value. Some kinds of values needs special processing. 183 | for k in range(numFields): 184 | key = keyDict[k] 185 | field = fields[k] 186 | if not field: 187 | continue 188 | if field in ["Default", "None", "FontBBox"]: 189 | # FontBBox is no longer supported - I calculate the real 190 | # instance fontBBox from the glyph metrics instead, 191 | continue 192 | if key == kFontName: 193 | value = field 194 | elif key in [kExtraGlyphs, kExceptionSuffixes]: 195 | value = eval(field) 196 | elif key in [kIsBoldKey, kIsItalicKey, kCoordsKey]: 197 | try: 198 | value = eval(field) # this works for all three fields. 199 | 200 | if key == kIsBoldKey: # need to convert to Type 1 field key. 201 | instanceDict[key] = value 202 | # add kForceBold key. 203 | key = kForceBold 204 | if value == 1: 205 | value = "true" 206 | else: 207 | value = "false" 208 | elif key == kIsItalicKey: 209 | if value == 1: 210 | value = "true" 211 | else: 212 | value = "false" 213 | elif key == kCoordsKey: 214 | if type(value) == type(0): 215 | value = (value,) 216 | except (NameError, SyntaxError): 217 | print "ERROR: In line %s, the %s field has an invalid value." % (i+1, key) 218 | parseError = 1 219 | continue 220 | 221 | elif field[0] in ["[","{"]: # it is a Type 1 array value. Turn it into a list and verify that there's an even number of values for the alignment zones 222 | value = field[1:-1].split() # Remove the begin and end brackets/braces, and make a list 223 | 224 | if key in kAlignmentZonesKeys: 225 | if len(value) % 2 != 0: 226 | print "ERROR: In line %s, the %s field does not have an even number of values." % (i+1, key) 227 | parseError = 1 228 | continue 229 | 230 | if key in kTopAlignZonesKeys: # The Type 1 spec only allows 7 top zones (7 pairs of values) 231 | if len(value) > kMaxTopZonesSize: 232 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxTopZonesSize) 233 | parseError = 1 234 | continue 235 | else: 236 | newArray = validateArrayValues(value, False) # False = values do NOT have to be all positive 237 | if newArray: 238 | value = newArray 239 | else: 240 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 241 | parseError = 1 242 | continue 243 | currentArray = value[:] # make copy, not reference 244 | value.sort() 245 | if currentArray != value: 246 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 247 | 248 | if key in kBotAlignZonesKeys: # The Type 1 spec only allows 5 top zones (5 pairs of values) 249 | if len(value) > kMaxBotZonesSize: 250 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxBotZonesSize) 251 | parseError = 1 252 | continue 253 | else: 254 | newArray = validateArrayValues(value, False) # False = values do NOT have to be all positive 255 | if newArray: 256 | value = newArray 257 | else: 258 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 259 | parseError = 1 260 | continue 261 | currentArray = value[:] # make copy, not reference 262 | value.sort() 263 | if currentArray != value: 264 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 265 | 266 | if key in kStdStemsKeys: 267 | if len(value) > kMaxStdStemsSize: 268 | print "ERROR: In line %s, the %s field can only have %d value." % (i+1, key, kMaxStdStemsSize) 269 | parseError = 1 270 | continue 271 | else: 272 | newArray = validateArrayValues(value, True) # True = all values must be positive 273 | if newArray: 274 | value = newArray 275 | else: 276 | print "ERROR: In line %s, the %s field has an invalid value." % (i+1, key) 277 | parseError = 1 278 | continue 279 | 280 | if key in kStemSnapKeys: # The Type 1 spec only allows 12 stem widths, including 1 standard stem 281 | if len(value) > kMaxStemSnapSize: 282 | print "ERROR: In line %s, the %s field has more than %d values." % (i+1, key, kMaxStemSnapSize) 283 | parseError = 1 284 | continue 285 | else: 286 | newArray = validateArrayValues(value, True) # True = all values must be positive 287 | if newArray: 288 | value = newArray 289 | else: 290 | print "ERROR: In line %s, the %s field contains invalid values." % (i+1, key) 291 | parseError = 1 292 | continue 293 | currentArray = value[:] # make copy, not reference 294 | value.sort() 295 | if currentArray != value: 296 | print "WARNING: In line %s, the values in the %s field were sorted in ascending order." % (i+1, key) 297 | else: 298 | # either a single number or a string. 299 | if re.match(r"^[-.\d]+$", field): 300 | value = field #it is a Type 1 number. Pass as is, as a string. 301 | else: 302 | value = field 303 | 304 | instanceDict[key] = value 305 | 306 | if (kStdHW in instanceDict and kStemSnapH not in instanceDict) or (kStdHW not in instanceDict and kStemSnapH in instanceDict): 307 | print "ERROR: In line %s, either the %s value or the %s values are missing or were invalid." % (i+1, kStdHW, kStemSnapH) 308 | parseError = 1 309 | elif (kStdHW in instanceDict and kStemSnapH in instanceDict): # cannot be just 'else' because it will generate a 'KeyError' when these hinting parameters are not provided in the 'instances' file 310 | if instanceDict[kStemSnapH][0] != instanceDict[kStdHW][0]: 311 | print "ERROR: In line %s, the first value in %s must be the same as the %s value." % (i+1, kStemSnapH, kStdHW) 312 | parseError = 1 313 | 314 | if (kStdVW in instanceDict and kStemSnapV not in instanceDict) or (kStdVW not in instanceDict and kStemSnapV in instanceDict): 315 | print "ERROR: In line %s, either the %s value or the %s values are missing or were invalid." % (i+1, kStdVW, kStemSnapV) 316 | parseError = 1 317 | elif (kStdVW in instanceDict and kStemSnapV in instanceDict): # cannot be just 'else' because it will generate a 'KeyError' when these hinting parameters are not provided in the 'instances' file 318 | if instanceDict[kStemSnapV][0] != instanceDict[kStdVW][0]: 319 | print "ERROR: In line %s, the first value in %s must be the same as the %s value." % (i+1, kStemSnapV, kStdVW) 320 | parseError = 1 321 | 322 | instancesList.append(instanceDict) 323 | 324 | if parseError or len(instancesList) == 0: 325 | raise(ParseError) 326 | 327 | return instancesList 328 | 329 | 330 | def saveCompositeInfo(fontMM, mmParentDir): 331 | filePath = os.path.join(mmParentDir, kCompositeDataName) 332 | numGlyphs = len(fontMM) 333 | glyphDict = {} 334 | numMasters = fontMM.glyphs[0].layers_number 335 | for gid in range(numGlyphs): 336 | glyph = fontMM.glyphs[gid] 337 | lenComps = len(glyph.components) 338 | if lenComps == 0: 339 | continue 340 | compList = [] 341 | glyphDict[glyph.name] = compList 342 | numBaseContours = glyph.GetContoursNumber() 343 | pathIndex = numBaseContours 344 | for cpi in range(lenComps): 345 | component = glyph.components[cpi] 346 | compGlyph = fontMM.glyphs[component.index] 347 | compName = compGlyph.name, 348 | compEntry = [compName, numBaseContours + cpi] 349 | metricsList = [None]*numMasters 350 | seenAnyChange = 0 351 | for mi in range(numMasters): 352 | shift = component.deltas[mi] 353 | scale = component.scales[mi] 354 | shiftEntry = scaleEntry = None 355 | if (shift.x != 0) or (shift.y != 0): 356 | shiftEntry = (shift.x, shift.y) 357 | if (scale.x != 1.0) or (scale.y !=1.0 ): 358 | scaleEntry = (scale.x, scale.y) 359 | if scaleEntry or shiftEntry: 360 | metricsEntry = (shiftEntry, scaleEntry) 361 | seenAnyChange = 1 362 | else: 363 | metricsEntry = None 364 | metricsList[mi] = metricsEntry 365 | compName = fontMM.glyphs[component.index].name 366 | if seenAnyChange: 367 | compList.append([compName, pathIndex, metricsList]) 368 | else: 369 | compList.append([compName, pathIndex, None]) 370 | pathIndex += compGlyph.GetContoursNumber() 371 | 372 | fp = open(filePath, "wt") 373 | fp.write(repr(glyphDict)) 374 | fp.close() 375 | 376 | 377 | def parseVals(valList): 378 | valList = valList.split() 379 | valList = map(eval, valList) 380 | return valList 381 | 382 | 383 | def fixFontBBox(data, pfaPath): 384 | bboxMatch = re.search(r"/FontBBox\s*\{\{([^}]+)\}\s*\{([^}]+)\}\s*\{([^}]+)\}\s*\{([^}]+)\}\}", data) 385 | if not bboxMatch: 386 | print "Failed to find MM FontBBox %s" % pfaPath 387 | return 388 | pfaBBox = [bboxMatch.group(1), bboxMatch.group(2), bboxMatch.group(3), bboxMatch.group(4)] 389 | pfaBBox = map(parseVals, pfaBBox) 390 | 391 | print "Calculating correct MM FontBBox..." 392 | 393 | mastersRange = range(fl.font.glyphs[0].layers_number) 394 | flBBox = [[], [], [], []] 395 | for i in range(4): 396 | for m in mastersRange: 397 | flBBox[i].append([]) 398 | c = 0 399 | for flGlyph in fl.font.glyphs: 400 | for m in mastersRange: 401 | bbox = flGlyph.GetBoundingRect(m) 402 | flBBox[0][m].append(bbox.ll.x) 403 | flBBox[1][m].append(bbox.ll.y) 404 | flBBox[2][m].append(bbox.ur.x) 405 | flBBox[3][m].append(bbox.ur.y) 406 | 407 | for m in mastersRange: 408 | flBBox[0][m] = int( round( min(flBBox[0][m])) ) 409 | flBBox[1][m] = int( round( min(flBBox[1][m])) ) 410 | flBBox[2][m] = int( round( max(flBBox[2][m])) ) 411 | flBBox[3][m] = int( round( max(flBBox[3][m])) ) 412 | if pfaBBox == flBBox: 413 | print "mmfont.pfa and fl.font have the same MM FontBBox values." 414 | else: 415 | matchGroups = bboxMatch.groups() 416 | numGroups = 4 # by definition of regex above. 417 | prefix = data[:bboxMatch.start(1)-1] 418 | postfix = data[bboxMatch.end(4)+1:] 419 | newString = [] 420 | for i in range(numGroups): 421 | newString.append("{") 422 | for m in mastersRange: 423 | newString.append 424 | newString.append("%s" % (flBBox[i][m]) ) 425 | newString.append("}") 426 | newString = " ".join(newString) 427 | data = prefix + newString + postfix 428 | try: 429 | fp = open(pfaPath, "wt") 430 | fp.write(data) 431 | fp.close() 432 | print "Updated mmfont.pfa with correct MM FontBBox values." 433 | except (OSError,IOError): 434 | print "Failed to open and write %s" % pfaPath 435 | 436 | 437 | def saveFiles(): 438 | try: 439 | parentDir = os.path.dirname(os.path.abspath(fl.font.file_name)) 440 | except AttributeError: 441 | print "The font has not been saved. Please save the font and try again." 442 | return 443 | 444 | instancesFilePath = os.path.join(parentDir, kInstancesDataFileName) 445 | 446 | if not os.path.isfile(instancesFilePath): 447 | print "Could not find the file named '%s' in the path below\n\t%s" % (kInstancesDataFileName, parentDir) 448 | return 449 | 450 | try: 451 | print "Parsing instances file..." 452 | instancesList = readInstanceFile(instancesFilePath) 453 | except ParseError: 454 | print "Error parsing file or file is empty." 455 | return 456 | 457 | # Set FontLab preferences 458 | flPrefs = Options() 459 | flPrefs.Load() 460 | flPrefs.T1Terminal = 0 # so we don't have to close the dialog with each instance. 461 | flPrefs.T1Encoding = 1 # always write Std Encoding. 462 | flPrefs.T1Decompose = 1 # Do decompose SEAC chars 463 | flPrefs.T1Autohint = 0 # Do not autohint unhinted chars 464 | 465 | # Generate mmfont.pfa 466 | pfaPath = os.path.join(parentDir, kDefaultMMFontFileName) 467 | print "Saving Type 1 MM font file to:%s\t%s" % (os.linesep, pfaPath) 468 | fl.GenerateFont(eval("ftTYPE1ASCII_MM"), pfaPath) 469 | 470 | # Check if mmfont.pfa was indeed generated 471 | if not os.path.exists(pfaPath): 472 | print "Failed to find %s" % pfaPath 473 | return 474 | 475 | # Save the composite glyph data, but only if it's necessary 476 | if (kExceptionSuffixes in instancesList[0] or kExtraGlyphs in instancesList[0]): 477 | compositePath = os.path.join(parentDir, kCompositeDataName) 478 | print "Saving composite glyphs data to:%s\t%s" % (os.linesep, compositePath) 479 | saveCompositeInfo(fl.font, parentDir) 480 | 481 | # Fix the FontBBox values if the font's UPM is not 1000 482 | if fl.font.upm != 1000: 483 | try: 484 | fp = open(pfaPath, "rt") 485 | data = fp.read() 486 | fp.close() 487 | except (OSError,IOError): 488 | print "Failed to open and read %s" % pfaPath 489 | return 490 | 491 | fixFontBBox(data, pfaPath) 492 | 493 | print "Done!" 494 | 495 | 496 | def run(): 497 | global debug 498 | if fl.count == 0: 499 | print 'No font opened.' 500 | return 501 | 502 | if len(fl.font) == 0: 503 | print 'The font has no glyphs.' 504 | return 505 | 506 | if fl.font[0].layers_number == 1: 507 | print 'The font is not MM.' 508 | return 509 | 510 | else: 511 | fl.output = '' 512 | saveFiles() 513 | 514 | 515 | if __name__ == "__main__": 516 | run() 517 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python scripts for FontLab 2 | ========================= 3 | Assortment of macros for [FontLab Studio](http://www.fontlab.com/font-editor/fontlab-studio/). 4 | 5 | Installation 6 | ----- 7 | 1. Download the [ZIP package](https://github.com/adobe-type-tools/fontlab-scripts/archive/master.zip) and unzip it. 8 | 2. Copy its contents to, 9 | - *Mac* `~/Library/Application Support/FontLab/Studio 5/Macros/` 10 | - *Win* `%systemdrive%\Users\%username%\Documents\FontLab\Studio5\Macros\` 11 | 12 | **OR** 13 | 14 | Run `installFontLabMacros.py` with this command, 15 | 16 | ```sh 17 | python installFontLabMacros.py 18 | ``` 19 | 3. Start FontLab Studio. 20 | 4. Turn on the **Macro** toolbar by selecting the menu *View > Toolbars > Macro*. 21 | -------------------------------------------------------------------------------- /TrueType/README.md: -------------------------------------------------------------------------------- 1 | #FontLab scripts for TrueType 2 | 3 | These scripts all are tailored to a TrueType hinting workflow that invoves 4 | FontLab. Successfully tested in the following versions of FL: 5 | 6 | - FontLab 5.1.2 Build 4447 (Mac) 7 | - FontLab 5.1.5 Build 5714 (Mac) 8 | - FontLab 5.2.1 Build 4868 (Win) 9 | 10 | Dependencies (for `tthDupe.py`): 11 | 12 | - Robofab 13 | - FontTools (`ttx`) 14 | 15 | ## IMPORTANT 16 | Mac users running OS 10.10 _Yosemite_ will run into several problems 17 | when trying to run those scripts from FontLab as they are used to. 18 | 19 | At the time of writing (April 2015), there is a bug in OSX Yosemite, which 20 | makes executing external code from FontLab difficult or even impossible. 21 | Since some of these scripts call the external `tx`, `ttx` and `type1` commands, 22 | this is a problem. 23 | 24 | In-depth description of this issue: 25 | http://forum.fontlab.com/index.php?topic=9134.0 26 | 27 | An easy workaround is launching FontLab from the command line, like this: 28 | 29 | open "/Applications/FontLab Studio 5.app" 30 | 31 | **This issue seems to have been fixed in Mac OS 10.11 _El Capitan_** 32 | 33 | 34 | ## Workflow 35 | 36 | This workflow relies on the Adobe style for building fonts, which means using 37 | a tree of folders, with one folder per style. 38 | 39 | 40 | #### Step 1 41 | The source files are UFOs. 42 | A `GlyphOrderAndAliasDB` file has been created in the root folder, which 43 | is a text file with (in most cases) two tab-separated columns. Those columns 44 | contain final glyph name and production glyph name for each glyph in the font. 45 | 46 | Roman 47 | ├─ GlyphOrderAndAliasDB 48 | │ 49 | ├─ Light 50 | │ └─ font.ufo 51 | │ 52 | ├─ Regular 53 | │ └─ font.ufo 54 | │ 55 | └─ Bold 56 | └─ font.ufo 57 | 58 | 59 | #### Step 2 60 | Run `convertToTTF.py` in Regular folder to get a VFB with TT outlines and a 61 | `font.ttf` file. 62 | 63 | Regular 64 | ├─ font.ttf 65 | ├─ font.ufo 66 | └─ font.vfb 67 | 68 | 69 | #### Step 3 70 | Hint the VFB file in FontLab, export the hints via `outputTTHints.py`, and 71 | export ppms via `outputPPMs.py`. This creates two new files, for storing 72 | this data externally. 73 | 74 | Regular 75 | ├─ font.ttf 76 | ├─ font.ufo 77 | ├─ font.vfb 78 | ├─ ppms 79 | └─ tthints 80 | 81 | 82 | #### Step 4 83 | If you don’t trust this script, this is a good time to create a backup copy of 84 | your hinted VFB. 85 | 86 | Then, run `convertToTTF.py` again; this time pick the root folder of your 87 | font project. This will result in new `font.vfb` and `font.ttf` files for 88 | each folder. If the conversion script finds `tthints` and `ppms` files in 89 | any of the folders, they are applied to the newly-generated VFB and TTF. 90 | 91 | Roman 92 | ├─ GlyphOrderAndAliasDB 93 | │ 94 | ├─ Light 95 | │ ├─ font.ttf 96 | │ ├─ font.ufo 97 | │ └─ font.vfb 98 | │ 99 | ├─ Regular 100 | │ ├─ font.ttf 101 | │ ├─ font.ufo 102 | │ ├─ font.vfb 103 | │ ├─ MyFont_backup.vfb 104 | │ ├─ ppms 105 | │ └─ tthints 106 | │ 107 | └─ Bold 108 | ├─ font.ttf 109 | ├─ font.ufo 110 | └─ font.vfb 111 | 112 | 113 | #### Step 5 114 | If outlines are compatible across weights, you can duplicate the TrueType hints 115 | with `tthDupe.py`. Note: outline compatibility must be given **with overlaps 116 | removed!** Run the script from FontLab, and first pick your template folder 117 | (in this case _Regular_), and then the root folder (in this case _Roman_). 118 | 119 | The script creates new tthints files in each of the non-template folders: 120 | 121 | Roman 122 | ├─ GlyphOrderAndAliasDB 123 | │ 124 | ├─ Light 125 | │ ├─ font.ttf 126 | │ ├─ font.ufo 127 | │ ├─ font.vfb 128 | │ └─ tthints 129 | │ 130 | ├─ Regular 131 | │ ├─ font.ttf 132 | │ ├─ font.ufo 133 | │ ├─ font.vfb 134 | │ ├─ MyFont_backup.vfb 135 | │ ├─ ppms 136 | │ └─ tthints 137 | │ 138 | └─ Bold 139 | ├─ font.ttf 140 | ├─ font.ufo 141 | ├─ font.vfb 142 | └─ tthints 143 | 144 | 145 | #### Step 6 146 | Those new `tthints` files can be applied to new VFBs, again by running 147 | `convertToTTF.py`. It is likely that some errors happen during the conversion, 148 | so it is recommeded to check the new VFBs and improve the tthints files for each 149 | style. The script `outputTTHints.py` helps with that, it will modify but not 150 | overwrite extend the existing `tthints` file. 151 | It is also recommended to check, adjust and export `ppms` for each style. 152 | 153 | 154 | #### Step 7 155 | When all `tthints` and `ppms` are perfect, we can create final `font.ttf` files 156 | for the whole project with `convertToTTF.py`. Then, those `font.ttf` are used to 157 | build the final, manually hinted TT fonts: 158 | 159 | makeotf -f font.ttf 160 | 161 | ---- 162 | 163 | ##### Background Info: 164 | 165 | When PS outlines are converted to TT outlines, the number of points for a given 166 | glyph is not always the same for all instances (even when generated from the 167 | same, compatible masters). TT curve segments can be described with almost any 168 | number of off-curve points. As a result, the `tthints` files created for 169 | one instance cannot simply b reused as they are with any another instance, 170 | even though the structure is similar. 171 | What `tthDupe.py` does is taking a note of the (on-curve) points TT instructions 172 | are attached to, to correlate them with the points in the original PS font. 173 | This information can be used for finding new point indexes in a related TrueType 174 | instance. 175 | 176 | 177 | See the Robothon 2012 talk on this problem here: 178 | [PDF slides with comments](http://typekit.files.wordpress.com/2012/04/truetype_hinting_robothon_2012.pdf) or [Video](http://vimeo.com/38352194) 179 | 180 | See the Robothon 2015 talk explaining the TT workflow here: 181 | [Video](https://vimeo.com/album/3329572/video/123813230) 182 | 183 | 184 | ## Scripts 185 | 186 | ### `convertToTTF.py` 187 | _FontLab menu name: **Convert PFA/UFO/TXT to TTF/VFB**_ 188 | Reads an UFO, PFA, or TXT font file and outputs both a raw `font.ttf` file and 189 | a `font.vfb` file. Those files may contain TT hints, which come from a 190 | conversion of PS hints. Therefore, it is recommended to `autohint` the input 191 | file before starting the TT conversion. 192 | If the script finds any `tthints` and `ppms` files the folder of the source file, 193 | this information is read and applied to output files. 194 | 195 | ### `inputTTHints.py` 196 | _FontLab menu name: **Input TrueType Hints**_ 197 | Reads an applies the contents of a `tthints` file to an open VFB. 198 | It is indifferent if this file references hints by point indexes or 199 | point coordinates. 200 | 201 | 202 | ### `outputPPMs.py` 203 | _FontLab menu name: **Output PPMs**_ 204 | Output PPMs (stem pixel jumps) as a simple `ppms` text file. 205 | 206 | 207 | ### `outputTTHints.py` 208 | _FontLab menu name: **Output TrueType Hints**_ 209 | Reads TT hints for selected glyphs and writes them to a simple `tthints` text 210 | file. The script will read all possible hinting instructions in both directions, 211 | and export all hints, also the hints attached to off-curve points. The idea is 212 | duplicating all FL hinting data in an external backup file. 213 | 214 | If the external file already exists, the script will replace existing 215 | entries in that file, and/or add new ones. The script will emit an error 216 | message if hints are attached to off-curve points, but still write them. 217 | 218 | 219 | ### `outputTTHints_coords.py` 220 | _FontLab menu name: **Output TrueType Hints\_coords**_ 221 | Like `outputTTHints.py`, but instead of point indexes, point coordinates are 222 | written. This script can be useful in the case that a TTF-VFB file has been 223 | created without the `convertToTTF.py` script (for instance directly from 224 | FontLab). 225 | Since `convertToTTF.py` makes some outline corrections (e.g. fixing double 226 | points, which FontLab will write) the resulting TT outlines might not 227 | perfectly match the FontLab exported TT-outlines, and therefore point indexes 228 | won’t match. 229 | Basically, the coordinate option is an attempt to save any hinting work that 230 | already has been done before using this workflow. 231 | The output data is written to a `tthints` file. 232 | 233 | This script imports `outputTTHints.py` as a module and therefore needs to be in 234 | the same folder. 235 | 236 | 237 | ### `tthDupe.py` 238 | _FontLab menu name: **TT Hints Duplicator**_ 239 | Macro to duplicate `tthints` files across compatible styles. The script was 240 | created with the idea of re-using existing hinting patterns across different 241 | weights, cutting the time investment for TT hinting by a significant amount. 242 | The script is to be run from within FontLab, and does not need any of the 243 | involved fonts to be open. 244 | 245 | 246 | ### `tthDupe_coords.py` 247 | _FontLab menu name: **TT Hints Duplicator\_coords**_ 248 | Like `tthDupe.py`, but instead of point indexes, point coordinates are written. 249 | 250 | This script imports `tthDupe.py` as a module and therefore needs to be in 251 | the same folder. 252 | 253 | 254 | __Important__: 255 | 256 | 1. `tthDupe.py` can only process TT instructions that are attached to *on-curve* 257 | points, because those are the only ones that will have the same coordinates 258 | in both PS and TT outlines. Source glyphs that have instructions attached to 259 | off-curve points will be dropped from the resulting `tthints` files. 260 | 261 | 2. The script will not duplicate Delta hints (by design), because Deltas are 262 | size- and style specific. They will simply be left out of the resulting 263 | `tthints` files. All the remaining hints of the glyph at hand will be written. 264 | 265 | 3. It is expected that overlaps are removed in the source files. This ensures 266 | outline predictability. Depending on the drawing, this can mean some work for 267 | compatibilizing all outlines, which is usually less work than hinting. 268 | 269 | 4. Hinting of sidebearings is currently not supported in the duplicator script. 270 | 271 | 5. Hinting of components is also currently not supported. While this is 272 | possible in theory, many FL crashes prevent proper testing and proper 273 | implementation of this desirable feature. 274 | 275 | -------------------------------------------------------------------------------- /TrueType/convertToTTF.py: -------------------------------------------------------------------------------- 1 | #FLM: Convert PFA/UFO/TXT to TTF/VFB 2 | # coding: utf-8 3 | 4 | __copyright__ = __license__ = """ 5 | Copyright (c) 2015-2016 Adobe Systems Incorporated. All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a 8 | copy of this software and associated documentation files (the "Software"), 9 | to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | and/or sell copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | """ 25 | 26 | __doc__ = """ 27 | Convert PFA/UFO/TXT to TTF/VFB 28 | 29 | This FontLab script will convert one or more hinted PFA/UFO or TXT files 30 | into TTF files, for use as input for makeOTF. 31 | The script will first ask for a directory, which usually should be the 32 | family's top-most folder. It will then crawl through that folder and 33 | process all input files it finds. In addition to the directory, the script 34 | will also ask for an encoding file. This encoding file is a FontLab '.enc' 35 | file which the script will use for ordering the glyphs. 36 | 37 | Note: 38 | This script imports the `Input TrueType Hints` script, therefore needs to be 39 | run from the same folder. 40 | 41 | ================================================== 42 | 43 | Versions: 44 | 45 | v1.7 - Jun 17 2016 - Skip 'prep' table processing if the font doesn't have it. 46 | v1.6 - Apr 25 2016 - Replace ttx commands by fontTools operations. 47 | v1.5 - Jul 17 2015 - Turn off the addition of NULL and CR glyphs. 48 | v1.4 - Apr 17 2015 - Support changes made to inputTTHints module. 49 | v1.3 - Apr 02 2015 - Now also works properly on FL Windows. 50 | v1.2 - Mar 26 2015 - Move code reading external `tthints` file to an adjacent 51 | module. 52 | v1.1 - Mar 23 2015 - Allow instructions in x-direction. 53 | v1.0 - Mar 04 2015 - Initial public release (Robothon 2015). 54 | 55 | """ 56 | 57 | import os 58 | import re 59 | import sys 60 | import time 61 | 62 | from FL import * 63 | import fl_cmd 64 | 65 | try: 66 | import dvInput_module 67 | dvModuleFound = True 68 | except: 69 | dvModuleFound = False 70 | 71 | fl.output = '' 72 | errorHappened = False 73 | 74 | # ---------------------------------------------------------------------------------------- 75 | # Find and import inputTTHints module: 76 | 77 | 78 | def findFile(fileName, path): 79 | 'Find file of given fileName, starting at path.' 80 | for root, dirs, files in os.walk(path): 81 | if fileName in files: 82 | return os.path.join(root) 83 | else: 84 | return None 85 | 86 | 87 | moduleName = 'inputTTHints.py' 88 | userFolder = os.path.expanduser('~') 89 | customModulePathMAC = os.sep.join(( 90 | userFolder, 'Library', 'Application Support', 91 | 'FontLab', 'Studio 5', 'Macros')) 92 | customModulePathPC = os.sep.join(( 93 | userFolder, 'Documents', 'FontLab', 'Studio5', 'Macros')) 94 | 95 | possibleModulePaths = [fl.userpath, customModulePathMAC, customModulePathPC] 96 | 97 | print '\nLooking for %s ... ' % (moduleName) 98 | for path in possibleModulePaths: 99 | modPath = findFile(moduleName, path) 100 | if modPath: 101 | print 'found at %s' % modPath 102 | break 103 | 104 | if not modPath: 105 | # Module was not found. World ends. 106 | errorHappened = True 107 | print 'Not found in the following folders:\n%s\n\ 108 | Please make sure the possibleModulePaths list in this script \ 109 | points to a folder containing %s' % ('\n'.join(possibleModulePaths), moduleName) 110 | 111 | else: 112 | # Module was found, import it. 113 | if modPath not in sys.path: 114 | sys.path.append(modPath) 115 | 116 | import inputTTHints 117 | 118 | # ---------------------------------------------------------------------------------------- 119 | 120 | MAC = False 121 | PC = False 122 | if sys.platform in ('mac', 'darwin'): 123 | MAC = True 124 | elif os.name == 'nt': 125 | PC = True 126 | 127 | 128 | # Add the FDK path to the env variable (on Mac only) so 129 | # that command line tools can be called from FontLab 130 | if MAC: 131 | fdkPathMac = os.sep.join(( 132 | userFolder, 'bin', 'FDK', 'tools', 'osx')) 133 | envPath = os.environ["PATH"] 134 | newPathString = envPath + ":" + fdkPathMac 135 | if fdkPathMac not in envPath: 136 | os.environ["PATH"] = newPathString 137 | if PC: 138 | from subprocess import Popen, PIPE 139 | 140 | # ---------------------------------------------------------------------------------------- 141 | # Import the FDK-embedded fontTools 142 | if MAC: 143 | osFolderName = "osx" 144 | if PC: 145 | osFolderName = "win" 146 | 147 | fontToolsPath = os.sep.join(( 148 | userFolder, 'bin', 'FDK', 'Tools', 149 | osFolderName, 'Python', 'AFDKOPython27', 'lib', 150 | 'python2.7', 'site-packages', 'FontTools')) 151 | 152 | if fontToolsPath not in sys.path: 153 | sys.path.append(fontToolsPath) 154 | 155 | try: 156 | from fontTools import ttLib 157 | except ImportError: 158 | print "\nERROR: FontTools Python module is not installed.\nGet the latest version at https://github.com/behdad/fonttools" 159 | errorHappened = True 160 | 161 | 162 | # ---------------------------------------------------------------------------------------- 163 | # constants: 164 | kPPMsFileName = "ppms" 165 | kTTHintsFileName = "tthints" 166 | kGOADBfileName = "GlyphOrderAndAliasDB" 167 | kTempEncFileName = ".tempEncoding" 168 | 169 | kFontTXT = "font.txt" 170 | kFontUFO = "font.ufo" 171 | kFontTTF = "font.ttf" 172 | 173 | flPrefs = Options() 174 | flPrefs.Load() 175 | 176 | 177 | def readFile(filePath): 178 | file = open(filePath, 'r') 179 | fileContent = file.read().splitlines() 180 | file.close() 181 | return fileContent 182 | 183 | 184 | def writeFile(contentList, filePath): 185 | outfile = open(filePath, 'w') 186 | outfile.writelines(contentList) 187 | outfile.close() 188 | 189 | 190 | def getFontPaths(path): 191 | fontsList = [] 192 | 193 | for root, folders, files in os.walk(path): 194 | fileAndFolderList = folders[:] 195 | fileAndFolderList.extend(files) 196 | 197 | pfaRE = re.compile(r'(^.+?\.pfa)$', re.IGNORECASE) 198 | ufoRE = re.compile(r'(^.+?\.ufo)$', re.IGNORECASE) 199 | txtRE = re.compile(r'^font.txt$', re.IGNORECASE) 200 | 201 | pfaFiles = [ 202 | match.group(1) for item in fileAndFolderList 203 | for match in [pfaRE.match(item)] if match] 204 | ufoFiles = [ 205 | match.group(1) for item in fileAndFolderList 206 | for match in [ufoRE.match(item)] if match] 207 | txtFiles = [ 208 | match.group(0) for item in fileAndFolderList 209 | for match in [txtRE.match(item)] if match] 210 | 211 | # Prioritizing the list of source files, so that only one of them is 212 | # found and converted; in case there are multiple possible files in 213 | # a single folder. Order of priority is PFA - UFO - TXT. 214 | allFontsFound = pfaFiles + ufoFiles + txtFiles 215 | 216 | if len(allFontsFound): 217 | item = allFontsFound[0] 218 | fontsList.append(os.path.join(root, item)) 219 | else: 220 | continue 221 | 222 | return fontsList 223 | 224 | 225 | def getGOADB2ndColumn(goadbList): 226 | 'Get the second column of the original GOADB file and return it as a list.' 227 | resultList = [] 228 | lineNum = 1 229 | skippedLines = 0 230 | re_match1stCol = re.compile(r"(\S+)\t(\S+)(\t\S+)?") 231 | 232 | for line in goadbList: 233 | # allow for comments: 234 | line = line.split('#')[0] 235 | # Skip over blank lines 236 | stripline = line.strip() 237 | if not stripline: 238 | skippedLines += 1 239 | continue 240 | 241 | result = re_match1stCol.match(line) 242 | if result: # the result can be None 243 | resultList.append(result.group(2) + '\n') 244 | else: # nothing matched 245 | print "Problem matching line %d (current GOADB)" % lineNum 246 | 247 | lineNum += 1 248 | 249 | if (len(goadbList) != (len(resultList) + skippedLines)): 250 | print "ERROR: There was a problem processing the current GOADB file" 251 | return None 252 | else: 253 | return resultList 254 | 255 | 256 | def makeTempEncFileFromGOADB(goadbPath): 257 | goadbFileContent = readFile(goadbPath) 258 | 259 | goadb2ndColumnList = getGOADB2ndColumn(goadbFileContent) 260 | if not goadb2ndColumnList: 261 | return None 262 | 263 | encPath = os.path.join(os.path.dirname(goadbPath), kTempEncFileName) 264 | writeFile(goadb2ndColumnList, encPath) 265 | return encPath 266 | 267 | 268 | def readPPMsFile(filePath): 269 | lines = readFile(filePath) 270 | 271 | hPPMsList = [] 272 | vPPMsList = [] 273 | 274 | for i in range(len(lines)): 275 | line = lines[i] 276 | # Skip over blank lines 277 | stripline = line.strip() 278 | if not stripline: 279 | continue 280 | # Get rid of all comments 281 | if line.find('#') >= 0: 282 | continue 283 | else: 284 | if "X:" in line: 285 | vPPMsList.append(line) 286 | else: 287 | hPPMsList.append(line) 288 | 289 | return hPPMsList, vPPMsList 290 | 291 | 292 | def replaceStemsAndPPMs(hPPMsList, vPPMsList): 293 | if len(hPPMsList) != len(fl.font.ttinfo.hstem_data): 294 | print "\tERROR: The amount of H stems does not match" 295 | return 296 | if len(vPPMsList) != len(fl.font.ttinfo.vstem_data): 297 | print "\tERROR: The amount of V stems does not match" 298 | return 299 | 300 | for i in range(len(fl.font.ttinfo.hstem_data)): 301 | name, width, ppm2, ppm3, ppm4, ppm5, ppm6 = hPPMsList[i].split('\t') 302 | stem = TTStem() 303 | stem.name = name 304 | stem.width = int(width) 305 | stem.ppm2 = int(ppm2) 306 | stem.ppm3 = int(ppm3) 307 | stem.ppm4 = int(ppm4) 308 | stem.ppm5 = int(ppm5) 309 | stem.ppm6 = int(ppm6) 310 | fl.font.ttinfo.hstem_data[i] = stem 311 | 312 | for i in range(len(fl.font.ttinfo.vstem_data)): 313 | name, width, ppm2, ppm3, ppm4, ppm5, ppm6 = vPPMsList[i].split('\t') 314 | stem = TTStem() 315 | stem.name = name 316 | stem.width = int(width) 317 | stem.ppm2 = int(ppm2) 318 | stem.ppm3 = int(ppm3) 319 | stem.ppm4 = int(ppm4) 320 | stem.ppm5 = int(ppm5) 321 | stem.ppm6 = int(ppm6) 322 | fl.font.ttinfo.vstem_data[i] = stem 323 | 324 | 325 | def processZonesArray(inArray): 326 | outArray = [] 327 | for x in range(len(inArray)/2): 328 | if inArray[x * 2] < 0: 329 | outArray.append(inArray[x * 2]) 330 | outArray.append(inArray[x * 2 + 1]) 331 | outArray.sort() 332 | return outArray 333 | 334 | 335 | def removeBottomZonesAboveBaseline(): 336 | baselineZonesWereRemoved = False 337 | # this is a single master font, so only the 338 | # first array will have non-zero values: 339 | newOtherBluesArray = processZonesArray(fl.font.other_blues[0]) 340 | if (fl.font.other_blues_num != len(newOtherBluesArray)): 341 | # trim the number of zones 342 | fl.font.other_blues_num = len(newOtherBluesArray) 343 | 344 | for x in range(len(newOtherBluesArray)): 345 | fl.font.other_blues[0][x] = newOtherBluesArray[x] 346 | 347 | baselineZonesWereRemoved = True 348 | 349 | newFamilyOtherBluesArray = processZonesArray(fl.font.family_other_blues[0]) 350 | if (fl.font.family_other_blues_num != len(newFamilyOtherBluesArray)): 351 | # trim the number of zones 352 | fl.font.family_other_blues_num = len(newFamilyOtherBluesArray) 353 | 354 | for x in range(len(newFamilyOtherBluesArray)): 355 | fl.font.family_other_blues[0][x] = newFamilyOtherBluesArray[x] 356 | 357 | baselineZonesWereRemoved = True 358 | 359 | return baselineZonesWereRemoved 360 | 361 | 362 | def replaceFontZonesByFamilyZones(): 363 | """ 364 | The font's zones are replaced by the family zones to make sure that all 365 | the styles have the same vertical height at all ppems. If the font doesn't 366 | have family zones (e.g. Regular style), don't do anything. 367 | """ 368 | fontZonesWereReplaced = False 369 | # TOP zones 370 | if len(fl.font.family_blues[0]): 371 | if fl.font.family_blues_num == 14 and fl.font.blue_values_num < fl.font.family_blues_num: 372 | print 373 | print "### MAJOR ERROR ###: Due to a FontLab bug the font's TOP zones cannot be replaced by the family TOP zones" 374 | print 375 | return fontZonesWereReplaced 376 | elif fl.font.family_blues_num == 14 and fl.font.blue_values_num == fl.font.family_blues_num: 377 | pass 378 | else: 379 | fl.font.blue_values_num = fl.font.family_blues_num 380 | # This will create a traceback if there are 7 top zones, 381 | # therefore the IFs above. 382 | 383 | # Replace the font's zones by the family zones 384 | for x in range(len(fl.font.family_blues[0])): 385 | fl.font.blue_values[0][x] = fl.font.family_blues[0][x] 386 | print "WARNING: The font's TOP zones were replaced by the family TOP zones." 387 | fontZonesWereReplaced = True 388 | 389 | # BOTTOM zones 390 | if len(fl.font.family_other_blues[0]): 391 | if fl.font.family_other_blues_num == 10 and fl.font.other_blues_num < fl.font.family_other_blues_num: 392 | print 393 | print "### MAJOR ERROR ###: Due to a FontLab bug the font's BOTTOM zones cannot be replaced by the family BOTTOM zones" 394 | print 395 | return fontZonesWereReplaced 396 | elif fl.font.family_other_blues_num == 10 and fl.font.other_blues_num == fl.font.family_other_blues_num: 397 | pass 398 | else: 399 | fl.font.other_blues_num = fl.font.family_other_blues_num 400 | # This will create a traceback if there are 5 bottom zones, 401 | # therefore the IFs above. 402 | 403 | # Replace the font's zones by the family zones 404 | for x in range(len(fl.font.family_other_blues[0])): 405 | fl.font.other_blues[0][x] = fl.font.family_other_blues[0][x] 406 | print "WARNING: The font's BOTTOM zones were replaced by the family BOTTOM zones." 407 | fontZonesWereReplaced = True 408 | 409 | return fontZonesWereReplaced 410 | 411 | 412 | def convertT1toTT(): 413 | ''' 414 | Converts an open FL font object from PS to TT outlines, using on-board 415 | FontLab commands. The outlines are post-processed to reset starting points 416 | to their original position. 417 | ''' 418 | for g in fl.font.glyphs: 419 | 420 | # Keeping track of original start point coordinates: 421 | startPointCoords = [ 422 | (point.x, point.y) for point in g.nodes if point.type == 17] 423 | 424 | # fl.TransformGlyph(g, 5, "0001") # Remove Horizontal Hints 425 | # fl.TransformGlyph(g, 5, "0003") # Remove Horizontal & Vertical Hints 426 | fl.TransformGlyph(g, 5, "0002") # Remove Vertical Hints 427 | fl.TransformGlyph(g, 13, "") # Curves to TrueType 428 | fl.TransformGlyph(g, 14, "0001") # Contour direction [TT] 429 | 430 | # The start points might move when FL reverses the contour. 431 | # This dictionary keeps track of the new coordinates. 432 | newCoordDict = { 433 | (node.x, node.y): index for index, node in enumerate(g.nodes)} 434 | 435 | # Going through all start points backwards, and re-setting them 436 | # to original position. 437 | for pointCoords in startPointCoords[::-1]: 438 | g.SetStartNode(newCoordDict[pointCoords]) 439 | 440 | fl.TransformGlyph(g, 7, "") # Convert PS hints to TT instructions. 441 | 442 | 443 | def changeTTfontSettings(): 444 | # Clear `gasp` array: 445 | if len(fl.font.ttinfo.gasp): 446 | del fl.font.ttinfo.gasp[0] 447 | # Create `gasp` element: 448 | gaspElement = TTGasp(65535, 2) 449 | # Range: 65535=0... 450 | # Options: 0=None 451 | # 1=Instructions 452 | # 2=Smoothing 453 | # 3=Instructions+Smoothing 454 | 455 | # Add element to `gasp` array 456 | fl.font.ttinfo.gasp[0] = gaspElement 457 | 458 | # Clear `hdmx` array 459 | for i in range(len(fl.font.ttinfo.hdmx)): 460 | try: 461 | del fl.font.ttinfo.hdmx[0] 462 | except: 463 | continue 464 | 465 | # Uncheck "Create [vdmx] table", also 466 | # uncheck "Automatically add .null, CR and space characters" 467 | fl.font.ttinfo.head_flags = 0 468 | 469 | 470 | def setType1openPrefs(): 471 | flPrefs.T1Decompose = 1 # checked - Decompose all composite glyphs 472 | flPrefs.T1Unicode = 0 # unchecked - Generate Unicode indexes for all glyphs 473 | flPrefs.OTGenerate = 0 # unchecked - Generate basic OpenType features for Type 1 fonts with Standard encoding 474 | flPrefs.T1MatchEncoding = 0 # unchecked - Find matching encoding table if possible 475 | 476 | 477 | def setTTgeneratePrefs(): 478 | flPrefs.TTENoReorder = 1 # unchecked - Automatically reorder glyphs 479 | flPrefs.TTEFontNames = 1 # option - Do not export OpenType name records 480 | flPrefs.TTESmartMacNames = 0 # unchecked - Use the OpenType names as menu names on Macintosh 481 | flPrefs.TTEStoreTables = 0 # unchecked - Write stored custom TrueType/OpenType tables 482 | flPrefs.TTEExportOT = 0 # unchecked - Export OpenType layout tables 483 | flPrefs.DSIG_Use = 0 # unchecked - Generate digital signature (DSIG table) 484 | flPrefs.TTEHint = 1 # checked - Export hinted TrueType fonts 485 | flPrefs.TTEKeep = 1 # checked - Write stored TrueType native hinting 486 | flPrefs.TTEVisual = 1 # checked - Export visual TrueType hints 487 | flPrefs.TTEAutohint = 0 # unchecked - Autohint unhinted glyphs 488 | flPrefs.TTEWriteBitmaps = 0 # unchecked - Export embedded bitmaps 489 | flPrefs.CopyHDMXData = 0 # unchecked - Copy HDMX data from base to composite glyph 490 | flPrefs.OTWriteMort = 0 # unchecked - Export "mort" table if possible 491 | flPrefs.TTEVersionOS2 = 3 # option - OS/2 table version 3 492 | flPrefs.TTEWriteKernTable = 0 # unchecked - Export old-style non-OpenType "kern" table 493 | flPrefs.TTEWriteKernFeature = 0 # unchecked - Generate OpenType "kern" feature if it is undefined or outdated 494 | flPrefs.TTECmap10 = 1 # option - Use following codepage to build cmap(1,0) table: 495 | # [Current codepage in the Font Window] 496 | flPrefs.TTEExportUnicode = 0 # checked - Ignore Unicode indexes in the font 497 | # option - Use following codepage for first 256 glyphs: 498 | # Do not reencode first 256 glyphs 499 | # unchecked - Export only first 256 glyphs of the selected codepage 500 | # unchecked - Put MS Char Set value into fsSelection field 501 | 502 | 503 | def setTTautohintPrefs(): 504 | # The single link attachment precision is 7 in all cases 505 | # flPrefs.TTHHintingOptions = 16135 # All options checked 506 | # flPrefs.TTHHintingOptions = 7 # All options unchecked 507 | flPrefs.TTHHintingOptions = 2055 # Cusps option checked 508 | 509 | 510 | def postProccessTTF(fontFilePath): 511 | ''' 512 | Post-process TTF font as generated by FontLab: 513 | - change FontLab-generated glyph name 'nonmarkingspace' to 'nbspace' 514 | - edit `prep` table to stop hints being active at 96 ppm and above. 515 | ''' 516 | print "Post-processing font.ttf file..." 517 | font = ttLib.TTFont(fontFilePath) 518 | glyphOrder = font.getGlyphOrder() 519 | postTable = font['post'] 520 | if 'prep' in font.keys(): 521 | prepTable = font['prep'] 522 | else: 523 | prepTable = None 524 | glyfTable = font['glyf'] 525 | hmtxTable = font['hmtx'] 526 | 527 | # Change name of 'nonbreakingspace' to 'nbspace' in GlyphOrder 528 | # and glyf table and add it to post table 529 | if "nonbreakingspace" in glyphOrder: 530 | # updateGlyphOrder = True 531 | glyphOrder[glyphOrder.index("nonbreakingspace")] = "nbspace" 532 | font.setGlyphOrder(glyphOrder) 533 | glyfTable.glyphs["nbspace"] = glyfTable.glyphs["nonbreakingspace"] 534 | del glyfTable.glyphs["nonbreakingspace"] 535 | hmtxTable.metrics["nbspace"] = hmtxTable.metrics["nonbreakingspace"] 536 | del hmtxTable.metrics["nonbreakingspace"] 537 | postTable.extraNames.append("nbspace") 538 | 539 | # Delete NULL and CR 540 | for gName in ["NULL", "nonmarkingreturn"]: 541 | if gName in glyphOrder: 542 | del glyphOrder[glyphOrder.index(gName)] 543 | font.setGlyphOrder(glyphOrder) 544 | del glyfTable.glyphs[gName] 545 | del hmtxTable.metrics[gName] 546 | if gName in postTable.extraNames: 547 | del postTable.extraNames[postTable.extraNames.index(gName)] 548 | 549 | # Extend the prep table 550 | # If the last byte is 551 | # WCVTP[ ] /* WriteCVTInPixels */ 552 | # add these extra bytes 553 | # MPPEM[ ] /* MeasurePixelPerEm */ 554 | # PUSHW[ ] /* 1 value pushed */ 555 | # 96 556 | # GT[ ] /* GreaterThan */ 557 | # IF[ ] /* If */ 558 | # PUSHB[ ] /* 1 value pushed */ 559 | # 1 560 | # ELSE[ ] /* Else */ 561 | # PUSHB[ ] /* 1 value pushed */ 562 | # 0 563 | # EIF[ ] /* EndIf */ 564 | # PUSHB[ ] /* 1 value pushed */ 565 | # 1 566 | # INSTCTRL[ ] /* SetInstrExecControl */ 567 | 568 | if prepTable: 569 | if prepTable.program.bytecode[-1] == 68: 570 | prepTable.program.bytecode.extend( 571 | [75, 184, 0, 96, 82, 88, 176, 1, 27, 176, 0, 89, 176, 1, 142]) 572 | 573 | # Save the changes 574 | folderPath, fontFileName = os.path.split(fontFilePath) 575 | newFontFilePath = os.path.join(folderPath, "%s%s" % ('_', fontFileName)) 576 | font.save(newFontFilePath) 577 | font.close() 578 | os.remove(fontFilePath) 579 | os.rename(newFontFilePath, fontFilePath) 580 | 581 | 582 | def convertTXTfontToPFA(txtPath): 583 | tempPFApath = txtPath.replace('.txt', '_TEMP_.pfa') 584 | command = 'type1 "%s" > "%s"' % (txtPath, tempPFApath) 585 | 586 | # Run type1 tool 587 | if MAC: 588 | pp = os.popen(command) 589 | # report = pp.read() 590 | pp.close() 591 | if PC: 592 | pp = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) 593 | out, err = pp.communicate() 594 | if err: 595 | print out, err 596 | 597 | return tempPFApath 598 | 599 | 600 | def convertUFOfontToPFA(ufoPath): 601 | tempPFApath = ufoPath.replace('.ufo', '_TEMP_.pfa') 602 | 603 | command = 'tx -t1 "%s" > "%s"' % (ufoPath, tempPFApath) 604 | 605 | # Run tx tool 606 | if MAC: 607 | pp = os.popen(command) 608 | # report = pp.read() 609 | pp.close() 610 | if PC: 611 | pp = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) 612 | out, err = pp.communicate() 613 | if err: 614 | print out, err 615 | 616 | return tempPFApath 617 | 618 | 619 | def processFonts(fontsList): 620 | totalFonts = len(fontsList) 621 | 622 | print "%d fonts found:\n%s\n" % (totalFonts, '\n'.join(fontsList)) 623 | 624 | setType1openPrefs() 625 | setTTgeneratePrefs() 626 | setTTautohintPrefs() 627 | 628 | fontIndex = 1 629 | for pfaPath in fontsList: 630 | 631 | # Make temporary encoding file from GOADB file. This step needs to 632 | # be done per font, because the directory tree selected may contain 633 | # more than one family, or because the glyph set of a given family 634 | # may not be the same for both Roman/Upright and Italic/Sloped. 635 | encPath = None 636 | goadbPath = None 637 | 638 | # The GOADB can be located in the same folder or up to two 639 | # levels above in the directory tree 640 | sameLevel = os.path.join(os.path.dirname(pfaPath), kGOADBfileName) 641 | oneUp = os.path.join( 642 | os.path.dirname(os.path.dirname(pfaPath)), kGOADBfileName) 643 | twoUp = os.path.join( 644 | os.path.dirname(os.path.dirname(os.path.dirname(pfaPath))), kGOADBfileName) 645 | 646 | if os.path.exists(sameLevel): 647 | goadbPath = sameLevel 648 | elif os.path.exists(oneUp): 649 | goadbPath = oneUp 650 | elif os.path.exists(twoUp): 651 | goadbPath = twoUp 652 | 653 | if goadbPath: 654 | encPath = makeTempEncFileFromGOADB(goadbPath) 655 | else: 656 | print "Could not find %s file." % kGOADBfileName 657 | print "Skipping %s" % pfaPath 658 | print 659 | 660 | if not encPath: 661 | continue 662 | 663 | # Checking if a derivedchars file exists. 664 | # If not, the dvInput step is skipped. 665 | makeDV = False 666 | 667 | for file in os.listdir(os.path.split(pfaPath)[0]): 668 | if re.search(r'derivedchars(.+?)?$', file) and dvModuleFound: 669 | makeDV = True 670 | 671 | fontIsTXT = False 672 | fontIsUFO = False 673 | 674 | if kFontTXT in pfaPath: 675 | fontIsTXT = True 676 | pfaPath = convertTXTfontToPFA(pfaPath) 677 | 678 | elif kFontUFO in pfaPath or (pfaPath[-4:].lower() in [".ufo"]): 679 | # Support more than just files named "font.ufo" 680 | fontIsUFO = True 681 | pfaPath = convertUFOfontToPFA(pfaPath) 682 | 683 | fl.Open(pfaPath) 684 | print "\nProcessing %s ... (%d/%d)" % ( 685 | fl.font.font_name, fontIndex, totalFonts) 686 | fontIndex += 1 687 | 688 | fontZonesWereReplaced = replaceFontZonesByFamilyZones() 689 | baselineZonesWereRemoved = removeBottomZonesAboveBaseline() 690 | 691 | # NOTE: After making changes to the PostScript alignment zones, the TT 692 | # equivalents have to be updated as well, but I couldn't find a way 693 | # to do it via scripting (because TTH.top_zones and TTH.bottom_zones 694 | # are read-only, and despite that functionality being available in 695 | # the UI, there's no native function to update TT zones from T1 zones). 696 | # So the solution is to generate a new T1 font and open it back. 697 | pfaPathTemp = pfaPath.replace('.pfa', '_TEMP_.pfa') 698 | infPathTemp = pfaPathTemp.replace('.pfa', '.inf') 699 | if baselineZonesWereRemoved or fontZonesWereReplaced: 700 | fl.GenerateFont(eval("ftTYPE1ASCII"), pfaPathTemp) 701 | fl[fl.ifont].modified = 0 702 | fl.Close(fl.ifont) 703 | fl.Open(pfaPathTemp) 704 | if os.path.exists(infPathTemp): 705 | # Delete the .INF file (bug in FL v5.1.x) 706 | os.remove(infPathTemp) 707 | 708 | # Load encoding file 709 | fl.font.encoding.Load(encPath) 710 | 711 | # Make sure the Font window is in 'Names mode' 712 | fl.CallCommand(fl_cmd.FontModeNames) 713 | 714 | # Sort glyphs by encoding 715 | fl.CallCommand(fl_cmd.FontSortByCodepage) 716 | 717 | # read derivedchars file, make components 718 | if makeDV: 719 | dvInput_module.run(verbose=False) 720 | 721 | convertT1toTT() 722 | changeTTfontSettings() 723 | 724 | # Switch the Font window to 'Index mode' 725 | fl.CallCommand(fl_cmd.FontModeIndex) 726 | 727 | # path to the folder containing the font, and the font's file name 728 | folderPath, fontFileName = os.path.split(pfaPath) 729 | ppmsFilePath = os.path.join(folderPath, kPPMsFileName) 730 | if os.path.exists(ppmsFilePath): 731 | hPPMs, vPPMs = readPPMsFile(ppmsFilePath) 732 | replaceStemsAndPPMs(hPPMs, vPPMs) 733 | 734 | tthintsFilePath = os.path.join(folderPath, kTTHintsFileName) 735 | if os.path.exists(tthintsFilePath): 736 | inputTTHints.run(folderPath) 737 | # readTTHintsFile(tthintsFilePath) 738 | # replaceTTHints() 739 | 740 | # FontLab 5.1.5 Mac Build 5714 does NOT respect the unchecked 741 | # option "Automatically add .null, CR and space characters" 742 | for gName in ["NULL", "CR"]: 743 | gIndex = fl.font.FindGlyph(gName) 744 | if gIndex != -1: 745 | del fl.font.glyphs[gIndex] 746 | 747 | vfbPath = pfaPath.replace('.pfa', '.vfb') 748 | fl.Save(vfbPath) 749 | 750 | # The filename of the TT output is hardcoded 751 | ttfPath = os.path.join(folderPath, kFontTTF) 752 | fl.GenerateFont(eval("ftTRUETYPE"), ttfPath) 753 | 754 | fl[fl.ifont].modified = 0 755 | fl.Close(fl.ifont) 756 | 757 | # The TT font generated with FontLab ends up with a few glyph names 758 | # changed. Fix the glyph names so that makeOTF does not fail. 759 | postProccessTTF(ttfPath) 760 | 761 | # Delete temporary Encoding file: 762 | if os.path.exists(encPath): 763 | os.remove(encPath) 764 | 765 | # Delete temp PFA: 766 | if os.path.exists(pfaPathTemp): 767 | os.remove(pfaPathTemp) 768 | 769 | # Cleanup after processing from TXT type1 font or UFO font 770 | if fontIsTXT or fontIsUFO: 771 | if os.path.exists(pfaPath): 772 | os.remove(pfaPath) 773 | if os.path.exists(ttfPath): 774 | finalTTFpath = ttfPath.replace('_TEMP_.ttf', '.ttf') 775 | if finalTTFpath != ttfPath: 776 | if PC: 777 | os.remove(finalTTFpath) 778 | os.rename(ttfPath, finalTTFpath) 779 | 780 | if os.path.exists(vfbPath): 781 | finalVFBpath = vfbPath.replace('_TEMP_.vfb', '.vfb') 782 | if finalVFBpath != vfbPath: 783 | if PC and os.path.exists(finalVFBpath): 784 | os.remove(finalVFBpath) 785 | os.rename(vfbPath, finalVFBpath) 786 | 787 | # remove FontLab leftovers 788 | pfmPath = pfaPathTemp.replace('.pfa', '.pfm') 789 | afmPath = pfaPathTemp.replace('.pfa', '.afm') 790 | if os.path.exists(pfmPath): 791 | os.remove(pfmPath) 792 | if os.path.exists(afmPath): 793 | os.remove(afmPath) 794 | 795 | 796 | def run(): 797 | # Get folder to process 798 | baseFolderPath = fl.GetPathName("Select font family directory") 799 | if not baseFolderPath: # Cancel was clicked or ESC key was pressed 800 | return 801 | 802 | startTime = time.time() 803 | 804 | fontsList = getFontPaths(baseFolderPath) 805 | 806 | if len(fontsList): 807 | processFonts(fontsList) 808 | else: 809 | print "No fonts found" 810 | 811 | endTime = time.time() 812 | elapsedSeconds = endTime-startTime 813 | 814 | if (elapsedSeconds/60) < 1: 815 | print '\nCompleted in %.1f seconds.\n' % elapsedSeconds 816 | else: 817 | print '\nCompleted in %d minutes and %d seconds.\n' % ( 818 | elapsedSeconds/60, elapsedSeconds%60) 819 | 820 | 821 | if __name__ == "__main__": 822 | if not errorHappened: 823 | run() 824 | -------------------------------------------------------------------------------- /TrueType/inputTTHints.py: -------------------------------------------------------------------------------- 1 | #FLM: Input TrueType Hints 2 | # coding: utf-8 3 | 4 | import os 5 | from FL import * 6 | import itertools 7 | 8 | __copyright__ = __license__ = """ 9 | Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a 12 | copy of this software and associated documentation files (the "Software"), 13 | to deal in the Software without restriction, including without limitation 14 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | and/or sell copies of the Software, and to permit persons to whom the 16 | Software is furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | """ 29 | 30 | __doc__ = """ 31 | Input TrueType Hints 32 | 33 | This FontLab macro will read an external simple text file `tthints` containing 34 | TrueType instructions and hinted point indexes or point coordinates for a 35 | number of glyphs, and will apply this data to the glyphs of an open VFB. 36 | 37 | ================================================== 38 | Versions: 39 | 40 | v1.4 - Apr 17 2015 - Remove unneeded coord_option, now that hints expressed 41 | with point coordinates are saved as 'tthints' instead of 42 | 'tthints_coords'. 43 | v1.3 - Mar 24 2015 - Enable reading 'tthints_coords' file with coordinates. 44 | v1.2 - Mar 23 2015 - Enable instructions in x-direction. 45 | v1.1 - Sep 07 2013 - Enable the reading of 'tthints' files with an optional 46 | column for glyph color mark. 47 | v1.0 - Jan 07 2013 - Initial release. 48 | """ 49 | 50 | # ---------------------------------------- 51 | 52 | debugMode = False 53 | report = [] 54 | 55 | # ---------------------------------------- 56 | 57 | vAlignLinkTop = 1 58 | vAlignLinkBottom = 2 59 | hSingleLink = 3 60 | vSingleLink = 4 61 | hDoubleLink = 5 62 | vDoubleLink = 6 63 | hAlignLinkNear = 7 64 | vAlignLinkNear = 8 65 | hInterpolateLink = 13 66 | vInterpolateLink = 14 67 | hMidDelta = 20 68 | vMidDelta = 21 69 | hFinDelta = 22 70 | vFinDelta = 23 71 | 72 | deltas = [hMidDelta, hFinDelta, vMidDelta, vFinDelta] 73 | interpolations = [hInterpolateLink, vInterpolateLink] 74 | links = [hSingleLink, hDoubleLink, vSingleLink, vDoubleLink] 75 | alignments = [vAlignLinkTop, vAlignLinkNear, vAlignLinkBottom, hAlignLinkNear] 76 | 77 | # ---------------------------------------- 78 | 79 | fuzziness = 2 80 | # Defines area to search for a point that might have moved 81 | # in transformation to TT. 82 | # 0 = 1 possibility (the exact point coordinates) 83 | # 1 = 9 possibilities (1 step in each direction from original point) 84 | # 2 = 25 possibilities (2 steps in each direction) 85 | # 3 = 49 possibilities (3 steps in each direction) etc. 86 | 87 | pointErrors = {} 88 | fuzzyPoints = {} 89 | 90 | # ---------------------------------------- 91 | 92 | 93 | def readTTHintsFile(filePath): 94 | file = open(filePath, "r") 95 | data = file.read() 96 | file.close() 97 | lines = data.splitlines() 98 | 99 | ttHintsList = [] 100 | 101 | for i in range(len(lines)): 102 | line = lines[i] 103 | # Skip over blank lines 104 | stripline = line.strip() 105 | if not stripline: 106 | continue 107 | # Skip over comments 108 | if line.find('#') >= 0: 109 | continue 110 | else: 111 | ttHintsList.append(line) 112 | 113 | return ttHintsList 114 | 115 | 116 | def findFuzzyPoint(glyphName, point, pointDict, fuzziness): 117 | ''' 118 | Finds points that fall inside a fuzzy area around 119 | the original coordinate. If only one point is found 120 | in that area, the point index will be returned. 121 | Otherwise returns None. 122 | 123 | Solves off-by-one issues. 124 | ''' 125 | 126 | fuzzyX = range(point[0]-fuzziness, point[0]+fuzziness+1,) 127 | fuzzyY = range(point[1]-fuzziness, point[1]+fuzziness+1,) 128 | possibleFuzzyPoints = [ 129 | fuzzyCoords for fuzzyCoords in itertools.product(fuzzyX, fuzzyY)] 130 | allPoints = pointDict.keys() 131 | overlap = set(allPoints) & set(possibleFuzzyPoints) 132 | 133 | if len(overlap) == 1: 134 | # make sure that only one point is found within the fuzzy area 135 | oldPoint = list(overlap)[0] 136 | pointIndex = pointDict[oldPoint] 137 | fuzzyPoints.setdefault(glyphName, []) 138 | if not (oldPoint, point) in fuzzyPoints[glyphName]: 139 | pointChange_msg = '\tINFO: In glyph {}, point #{} has changed from {} to {}.'.format( 140 | glyphName, pointIndex, oldPoint, point) 141 | report.append(pointChange_msg) 142 | print pointChange_msg 143 | fuzzyPoints[glyphName].append((oldPoint, point)) 144 | 145 | return pointIndex 146 | else: 147 | return None 148 | 149 | 150 | def transformCommandList(glyph, raw_commandList): 151 | ''' 152 | Transforms a list of commands with point coordinates 153 | to an list of commands with point indexes, for instance: 154 | 155 | input: [4, (155, 181), (180, 249), 0, -1] 156 | output: [4, 6, 9, 0, -1] 157 | 158 | input: [3, 'BL', (83, 0), 0, -1] 159 | output: [3, 34, 0, 0, -1] 160 | 161 | Also is used to check validity of point coordinates, and 162 | transforming sidebearing flags to point indexes. 163 | 164 | ''' 165 | 166 | pointDict = dict( 167 | ((point.x, point.y), pointIndex) 168 | for pointIndex, point in enumerate(glyph.nodes)) 169 | output = [] 170 | 171 | for item in raw_commandList: 172 | if item == 'BL': 173 | # left sidebearing hinted 174 | output.append(len(glyph)) 175 | elif item == 'BR': 176 | # right sidebearing hinted 177 | output.append(len(glyph) + 1) 178 | elif isinstance(item, tuple): 179 | # point coordinates 180 | pointIndex = pointDict.get(item, None) 181 | 182 | if pointIndex is None: 183 | # Try fuzziness if no exact coordinate match is found: 184 | fuzzyPointIndex = findFuzzyPoint( 185 | glyph.name, item, pointDict, fuzziness) 186 | if fuzzyPointIndex is not None: 187 | pointIndex = fuzzyPointIndex 188 | else: 189 | pointErrors.setdefault(glyph.name, []) 190 | if item not in pointErrors[glyph.name]: 191 | pointError_msg = '\tERROR: point %s does not exist in glyph %s.' % ( 192 | item, glyph.name) 193 | report.append(pointError_msg) 194 | print pointError_msg 195 | pointErrors[glyph.name].append(item) 196 | 197 | output.append(pointIndex) 198 | else: 199 | 'other hinting data, integers' 200 | output.append(item) 201 | 202 | if None in output: 203 | # a point was not found at all, so no hinting recipe is returned 204 | return [] 205 | else: 206 | return output 207 | 208 | 209 | def applyTTHints(ttHintsList): 210 | glyphsHinted = 0 211 | for line in ttHintsList: 212 | hintItems = line.split("\t") 213 | 214 | if len(hintItems) == 3: 215 | # line contains glyph name, hint info and mark color 216 | pass 217 | 218 | elif len(hintItems) == 2: 219 | # line does not contain mark color 220 | hintItems.append(80) # green 221 | 222 | else: 223 | hintDefError_msg = "ERROR: This hint definition does not have the correct format\n\t%s" % line 224 | report.append(hintDefError_msg) 225 | print hintDefError_msg 226 | continue 227 | 228 | gName, gHintsString, gMark = hintItems 229 | gIndex = fl.font.FindGlyph(gName) 230 | 231 | if gIndex != -1: 232 | glyph = fl.font[gName] 233 | else: 234 | print "ERROR: Glyph %s not found in the font." % gName 235 | continue 236 | 237 | if not len(gHintsString.strip()): 238 | print "WARNING: There are no hints defined for glyph %s." % gName 239 | continue 240 | 241 | gHintsList = gHintsString.split(";") 242 | 243 | tth = TTH(glyph) 244 | tth.LoadProgram(glyph) 245 | tth.ResetProgram() 246 | 247 | if debugMode: 248 | print gName 249 | 250 | readingError = False 251 | for commandString in gHintsList: 252 | raw_commandList = list(eval(commandString)) 253 | 254 | commandType = raw_commandList[0] 255 | commandList = transformCommandList(glyph, raw_commandList) 256 | 257 | if not commandList: 258 | readingError = True 259 | continue 260 | 261 | if len(commandList) < 3: 262 | print "ERROR: A hint definition for glyph %s does not have enough parameters: %s" % ( 263 | gName, commandString) 264 | continue 265 | 266 | # Create the TTHCommand 267 | try: 268 | ttc = TTHCommand(commandType) 269 | except RuntimeError: 270 | print "ERROR: A hint definition for glyph %s has an invalid command type: %s\n\t\tThe first value must be within the range %s-%s." % ( 271 | gName, commandType, vAlignLinkTop, vFinDelta) 272 | continue 273 | 274 | paramError = False 275 | 276 | if commandType in deltas: 277 | nodes = [commandList[1]] 278 | elif commandType in links: 279 | nodes = commandList[1:3] 280 | elif commandType in alignments + interpolations: 281 | nodes = commandList[1:-1] 282 | else: 283 | print "WARNING: Hint type %d in glyph %s is not supported." % ( 284 | commandType, gName) 285 | paramError = True 286 | nodes = [] 287 | 288 | for nodeIndex in nodes: 289 | try: 290 | gNode = glyph.nodes[nodeIndex] 291 | except IndexError: 292 | if nodeIndex in range(len(glyph), len(glyph)+2): 293 | pass 294 | else: 295 | print "ERROR: A hint definition for glyph %s is referencing an invalid node index: %s" % ( 296 | gName, nodeIndex) 297 | paramError = True 298 | break 299 | 300 | for i, item in enumerate(commandList[1:]): 301 | ttc.params[i] = item 302 | 303 | if not paramError: 304 | tth.commands.append(ttc) 305 | 306 | if readingError: 307 | readError_msg = '\tProblems with reading hinting recipe of glyph %s\n' % ( 308 | gName) 309 | report.append(readError_msg) 310 | print readError_msg 311 | 312 | if len(tth.commands): 313 | if readingError: 314 | glyph.mark = 1 # red 315 | else: 316 | if glyph.name in fuzzyPoints.keys(): 317 | glyph.mark = 30 # orange 318 | else: 319 | glyph.mark = int(gMark) # green 320 | tth.SaveProgram(glyph) 321 | fl.UpdateGlyph(gIndex) 322 | glyphsHinted += 1 323 | 324 | if glyphsHinted > 0: 325 | fl.font.modified = 1 326 | 327 | 328 | def run(parentDir): 329 | kTTHintsFileName = "tthints" 330 | kReportFileName = "ttinput_report.txt" 331 | 332 | tthintsFilePath = os.path.join(parentDir, kTTHintsFileName) 333 | reportFilePath = os.path.join(parentDir, kReportFileName) 334 | 335 | if os.path.exists(tthintsFilePath): 336 | print 'Reading', tthintsFilePath 337 | report.append(tthintsFilePath) 338 | ttHintsList = readTTHintsFile(tthintsFilePath) 339 | else: 340 | print "Could not find the %s file at %s" % ( 341 | kTTHintsFileName, tthintsFilePath) 342 | return 343 | 344 | if len(ttHintsList): 345 | applyTTHints(ttHintsList) 346 | print "TT hints added." 347 | else: 348 | print "The %s file at %s has no hinting data." % ( 349 | kTTHintsFileName, tthintsFilePath) 350 | return 351 | 352 | if len(report) > 1: 353 | # Write the error report to a file, because FL's output 354 | # window is a bit too ephemeral. 355 | with open(reportFilePath, 'w') as rf: 356 | rf.write('\n'.join(report)) 357 | elif len(report) <= 1 and os.path.exists(reportFilePath): 358 | os.remove(reportFilePath) 359 | del report[:] 360 | 361 | 362 | def preRun(): 363 | # Clear the Output window 364 | fl.output = '\n' 365 | 366 | if fl.count == 0: 367 | print "Please open a font first." 368 | return 369 | 370 | font = fl.font 371 | 372 | if len(font) == 0: 373 | print "The font has no glyphs." 374 | return 375 | 376 | try: 377 | parentDir = os.path.dirname(os.path.realpath(font.file_name)) 378 | except AttributeError: 379 | print "The font has not been saved. Please save the font and try again." 380 | return 381 | 382 | run(parentDir) 383 | 384 | 385 | if __name__ == "__main__": 386 | preRun() 387 | -------------------------------------------------------------------------------- /TrueType/outputPPMs.py: -------------------------------------------------------------------------------- 1 | #FLM: Output PPMs 2 | # coding: utf-8 3 | 4 | import os 5 | from FL import fl 6 | 7 | __copyright__ = __license__ = """ 8 | Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the "Software"), 12 | to deal in the Software without restriction, including without limitation 13 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 | and/or sell copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | """ 28 | 29 | __doc__ = """ 30 | Output PPMs 31 | 32 | This script will write (or overwrite) a `ppms` file in the same directory 33 | as the opened VFB file. This `ppms` file contains the TrueType stem values 34 | and the ppm values at which the pixel jumps occur. These values can later 35 | be edited as the `ppms` file is used as part of the conversion process. 36 | 37 | ================================================== 38 | Versions: 39 | 40 | v1.0 - Mar 27 2015 - First public release. 41 | 42 | """ 43 | 44 | kPPMsFileName = "ppms" 45 | 46 | 47 | def collectPPMs(): 48 | ppmsList = ["#Name\tWidth\tppm2\tppm3\tppm4\tppm5\tppm6\n"] 49 | for x in fl.font.ttinfo.hstem_data: 50 | hstem = '%s\t%d\t%d\t%d\t%d\t%d\t%d\n' % ( 51 | x.name, x.width, x.ppm2, x.ppm3, x.ppm4, x.ppm5, x.ppm6) 52 | ppmsList.append(hstem) 53 | 54 | for y in fl.font.ttinfo.vstem_data: 55 | vstem = '%s\t%d\t%d\t%d\t%d\t%d\t%d\n' % ( 56 | y.name, y.width, y.ppm2, y.ppm3, y.ppm4, y.ppm5, y.ppm6) 57 | ppmsList.append(vstem) 58 | 59 | return ppmsList 60 | 61 | 62 | def writePPMsFile(content): 63 | # path to the folder where the font is contained and the font's file name: 64 | folderPath, fontFileName = os.path.split(fl.font.file_name) 65 | filePath = os.path.join(folderPath, kPPMsFileName) 66 | outfile = open(filePath, 'w') 67 | outfile.writelines(content) 68 | outfile.close() 69 | 70 | 71 | def run(): 72 | if len(fl): 73 | if (fl.font.file_name is None): 74 | print "ERROR: You must save the VFB first." 75 | return 76 | 77 | if len(fl.font.ttinfo.hstem_data): 78 | ppmsList = collectPPMs() 79 | writePPMsFile(ppmsList) 80 | print "Done!" 81 | else: 82 | print "ERROR: The font has no TT stems data." 83 | 84 | else: 85 | print "ERROR: No font opened." 86 | 87 | 88 | if __name__ == "__main__": 89 | run() 90 | -------------------------------------------------------------------------------- /TrueType/outputTTHint_coords.py: -------------------------------------------------------------------------------- 1 | #FLM: Output TrueType Hints_coords 2 | # coding: utf-8 3 | 4 | import os 5 | import sys 6 | 7 | __copyright__ = __license__ = """ 8 | Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the "Software"), 12 | to deal in the Software without restriction, including without limitation 13 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 | and/or sell copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | """ 28 | 29 | __doc__ = """ 30 | Output TrueType Hints_coords 31 | 32 | This FontLab macro will write an external simple text file `tthints` which 33 | contains TrueType instructions and coordinates of hinted points for each 34 | selected glyph of an open VFB. 35 | If the external file already exists, the script will replace the existing 36 | entries and add new entries as needed. 37 | 38 | Example output: 39 | n 5,(447,0),(371,0),1;5,(148,0),(73,0),1;1,(314,459),0;1,(54,450),0;2,(73,0),0;2,(371,0),0;14,(141,394),(314,459),(73,0),0;4,(314,459),(286,392),0,-1;14,(148,339),(286,392),(73,0),0 40 | o 5,(124,226),(44,223),0;5,(453,225),(373,223),0;1,(252,459),0;2,(248,-8),0;4,(248,-8),(251,59),0,-1;4,(252,459),(248,392),0,-1 41 | 42 | Note: 43 | This script imports the `Output TrueType Hints` script, therefore needs to be 44 | run from the same folder. 45 | 46 | ================================================== 47 | Versions: 48 | 49 | v1.1 - Apr 17 2015 - Change name of output file from 'tthints_coords' to 'tthints'. 50 | v1.0 - Mar 31 2015 - First public release. 51 | """ 52 | 53 | 54 | def findFile(fileName, path): 55 | 'Find file of given fileName, starting at path.' 56 | for root, dirs, files in os.walk(path): 57 | if fileName in files: 58 | return os.path.join(root) 59 | else: 60 | return None 61 | 62 | 63 | moduleName = 'outputTTHints.py' 64 | userFolder = os.path.expanduser('~') 65 | customModulePathMAC = os.path.join( 66 | userFolder, 'Library', 'Application Support', 67 | 'FontLab', 'Studio 5', 'Macros') 68 | customModulePathPC = os.path.join( 69 | userFolder, 'Documents', 'FontLab', 'Studio5', 'Macros') 70 | customModulePathMAC = os.path.expanduser(customModulePathMAC) 71 | customModulePathPC = os.path.expanduser(customModulePathPC) 72 | possibleModulePaths = [fl.userpath, customModulePathMAC, customModulePathPC] 73 | 74 | print '\nLooking for %s ... ' % (moduleName) 75 | for path in possibleModulePaths: 76 | modPath = findFile(moduleName, path) 77 | if modPath: 78 | print 'found at %s' % modPath 79 | break 80 | 81 | if not modPath: 82 | # Module was not found. World ends. 83 | print 'Not found in the following folders:\n%s\n\ 84 | Please make sure the possibleModulePaths list in this script \ 85 | points to a folder containing %s' % ('\n'.join(possibleModulePaths), moduleName) 86 | 87 | else: 88 | # Module was found, import it and run it. 89 | if modPath not in sys.path: 90 | sys.path.append(modPath) 91 | 92 | import outputTTHints 93 | outputTTHints.preRun(coord_option=True) 94 | -------------------------------------------------------------------------------- /TrueType/outputTTHints.py: -------------------------------------------------------------------------------- 1 | #FLM: Output TrueType Hints 2 | # coding: utf-8 3 | 4 | import os 5 | from FL import * 6 | 7 | __copyright__ = __license__ = """ 8 | Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the "Software"), 12 | to deal in the Software without restriction, including without limitation 13 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 | and/or sell copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | """ 28 | 29 | __doc__ = """ 30 | Output TrueType Hints 31 | 32 | This FontLab macro will write a simple text file `tthints` which contains 33 | TrueType instructions and point indexes for each selected glyph. 34 | If the external file already exists, the script will replace the existing 35 | entries and add new entries as needed. 36 | 37 | The script will emit an error if there are hints attached to off-curve 38 | points, but will still write those hints. If hints are attached to nonexistent 39 | points, the given glyph will not be written to the output file. 40 | 41 | Example output: 42 | n 5,13,14,1;5,22,0,1;1,9,0;1,2,0;2,0,0;2,14,0;14,4,9,0,0;4,9,18,0,-1;14,21,18,0,0 43 | o 5,33,5,0;5,13,23,0;1,10,0;2,0,0;4,0,18,0,-1;4,10,28,0,-1 44 | 45 | 46 | ================================================== 47 | Versions: 48 | 49 | v1.5 - Nov 16 2015 - Do not write empty output data to tthints file 50 | (A bug which affected glyphs in which a nonexistent point 51 | was hinted. Reading those files made the reader choke.) 52 | v1.4 - Apr 17 2015 - Always write to a file named 'tthints' (even if the 53 | data uses point coordinates instead of point indexes). 54 | v1.3 - Mar 25 2015 - Allow optional coordinate output. 55 | Allow hinting sidebearings. 56 | v1.2 - Mar 23 2015 – Allow instructions in x-direction. 57 | v1.1 - Mar 04 2015 – Change name to Output TrueType Hints to supersede 58 | the old script of the same name. 59 | v1.0 - Nov 27 2013 - Initial release 60 | """ 61 | 62 | vAlignLinkTop = 1 63 | vAlignLinkBottom = 2 64 | hSingleLink = 3 65 | vSingleLink = 4 66 | hDoubleLink = 5 67 | vDoubleLink = 6 68 | hAlignLinkNear = 7 69 | vAlignLinkNear = 8 70 | hInterpolateLink = 13 71 | vInterpolateLink = 14 72 | hMidDelta = 20 73 | vMidDelta = 21 74 | hFinDelta = 22 75 | vFinDelta = 23 76 | 77 | deltas = [hMidDelta, hFinDelta, vMidDelta, vFinDelta] 78 | interpolations = [hInterpolateLink, vInterpolateLink] 79 | links = [hSingleLink, hDoubleLink, vSingleLink, vDoubleLink] 80 | alignments = [vAlignLinkTop, vAlignLinkNear, vAlignLinkBottom, hAlignLinkNear] 81 | 82 | 83 | def readTTHintsFile(filePath): 84 | file = open(filePath, "r") 85 | data = file.read() 86 | file.close() 87 | lines = data.splitlines() 88 | 89 | ttHintsList = [] 90 | 91 | for i in range(len(lines)): 92 | line = lines[i] 93 | # Skip over blank lines 94 | stripline = line.strip() 95 | if not stripline: 96 | continue 97 | # Skip over comments 98 | if line.find('#') >= 0: 99 | continue 100 | else: 101 | ttHintsList.append(line) 102 | 103 | return ttHintsList 104 | 105 | 106 | def writeTTHintsFile(content, filePath): 107 | outfile = open(filePath, 'w') 108 | outfile.writelines(content) 109 | outfile.close() 110 | 111 | 112 | def processTTHintsFileData(ttHintsList): 113 | storedGlyphs = [] 114 | ttHintsDict = {} 115 | 116 | for item in ttHintsList: 117 | hintItems = item.split("\t") 118 | 119 | if len(hintItems) not in [2, 3]: 120 | print "\tERROR: This hint definition does not have the correct format\n\t%s" % item 121 | continue 122 | 123 | gName = hintItems[0] 124 | ttHintsDict[gName] = item 125 | storedGlyphs.append(gName) 126 | 127 | return storedGlyphs, ttHintsDict 128 | 129 | 130 | def analyzePoint(glyph, nodeIndex): 131 | ''' 132 | Analyzes a given point for a given glyph. 133 | In a normal case, this function returns a tuple of point index and point 134 | coordinates. If the sidebearings have been hinted, this function returns 135 | a tuple containing the flag for the appropriate sidebearing. 136 | ''' 137 | 138 | point = glyph[nodeIndex] 139 | try: 140 | point.type 141 | 142 | except: 143 | ''' 144 | This happens if left or right bottom point have been hinted. 145 | In the hinting code, those points are stored as point indices 146 | greater than the actual point count of the glyph. 147 | Since those points cannot be grasped in the context of the outline, 148 | flags are created: 149 | 150 | "BL" is bottom left 151 | "BR" is bottom right. 152 | 153 | Those flags are written into the output file as strings with quotes, 154 | so they can be parsed with eval() when reading the file later. 155 | ''' 156 | 157 | if nodeIndex == len(glyph): 158 | point_flag = '"BL"' 159 | elif nodeIndex == len(glyph) + 1: 160 | point_flag = '"BR"' 161 | else: 162 | 'Point not in glyph -- which should not happen, but you never know.' 163 | print '\tERROR: Point index problem in glyph %s' % glyph.name 164 | return None, None 165 | 166 | return point_flag, point_flag 167 | 168 | point_coordinates = point.x, point.y 169 | 170 | if point.type == nOFF: 171 | gName = glyph.name 172 | print "\tWARNING: Off-curve point %s hinted in glyph %s." % ( 173 | point_coordinates, gName) 174 | 175 | return nodeIndex, point_coordinates 176 | 177 | 178 | def collectInstructions(tth, glyph, coord_option): 179 | ''' 180 | Parses tth commands (list of integers) and returns them formatted 181 | for writing the data into an external text file. 182 | 183 | tth data structure: 184 | ------------------- 185 | 186 | alignment points: 187 | [command code, point index, alignment] 188 | 189 | single and double links: 190 | [command code, point index 1, point index 2, stem ID, alignment] 191 | 192 | interpolations: 193 | [command code, point index 1, point index 2, point index 3, alignment] 194 | 195 | deltas: 196 | [command code, point index, offset, point range min, point range max] 197 | ''' 198 | 199 | coord_commandsList = [] 200 | index_commandsList = [] 201 | 202 | for inst in tth.commands: 203 | command = [inst.code] 204 | coord_command = [inst.code] 205 | index_command = [inst.code] 206 | 207 | for p in inst.params: 208 | 'create raw tth command list to parse' 209 | command.append(p) 210 | 211 | if inst.code in deltas: 212 | 'delta' 213 | nodeIndex = command[1] 214 | deltaDetails = command[2:] 215 | point_index, point_coordinates = analyzePoint(glyph, nodeIndex) 216 | 217 | coord_command.append(point_coordinates) 218 | index_command.append(point_index) 219 | coord_command.extend(deltaDetails) 220 | index_command.extend(deltaDetails) 221 | 222 | elif inst.code in links: 223 | 'single- or double link' 224 | linkDetails = command[3:] 225 | for nodeIndex in command[1:3]: 226 | point_index, point_coordinates = analyzePoint(glyph, nodeIndex) 227 | 228 | coord_command.append(point_coordinates) 229 | index_command.append(point_index) 230 | coord_command.extend(linkDetails) 231 | index_command.extend(linkDetails) 232 | 233 | elif inst.code in alignments + interpolations: 234 | 'alignment or interpolation' 235 | alignmentDetails = command[-1] 236 | for nodeIndex in command[1:-1]: 237 | point_index, point_coordinates = analyzePoint(glyph, nodeIndex) 238 | 239 | coord_command.append(point_coordinates) 240 | index_command.append(point_index) 241 | coord_command.append(alignmentDetails) 242 | index_command.append(alignmentDetails) 243 | 244 | else: 245 | 'unknown instruction code' 246 | print "\tERROR: Hinting problem in glyph %s." % glyph.name 247 | coord_command = [] 248 | index_command = [] 249 | 250 | if None in coord_command: 251 | ' hinting error has been picked up by anyalyzePoint()' 252 | return [] 253 | 254 | coord_command_string = ','.join(map(str, coord_command)) 255 | coord_command_string = coord_command_string.replace(' ', '') 256 | index_command_string = ','.join(map(str, index_command)) 257 | 258 | index_commandsList.append(index_command_string) 259 | coord_commandsList.append(coord_command_string) 260 | 261 | if coord_option: 262 | return coord_commandsList 263 | else: 264 | return index_commandsList 265 | 266 | 267 | def processGlyphs(selectedGlyphs, storedGlyphs, ttHintsDict, coord_option): 268 | # Iterate through all the glyphs instead of just the ones selected. 269 | # This way the order of the items in the output file will be constant 270 | # and predictable. 271 | 272 | allGlyphsHintList = ["# Glyph name\tTT hints\n"] 273 | 274 | for glyph in fl.font.glyphs: 275 | gName = glyph.name 276 | 277 | if (gName in selectedGlyphs) or (gName in storedGlyphs): 278 | if gName in selectedGlyphs: 279 | # If the glyph is selected, read its hints. 280 | tth = TTH(glyph) 281 | tth.LoadProgram() 282 | 283 | if len(tth.commands): 284 | instructionsList = collectInstructions( 285 | tth, glyph, coord_option) 286 | if len(instructionsList): 287 | gHints = "%s\t%s\n" % ( 288 | gName, ';'.join(instructionsList)) 289 | allGlyphsHintList.append(gHints) 290 | else: 291 | print "WARNING: The glyph %s has no TrueType hints." % gName 292 | else: 293 | # If the glyph is not selected but present in the external 294 | # file, just read the data from the ttHintsDict and dump it. 295 | allGlyphsHintList.append(ttHintsDict[gName] + '\n') 296 | 297 | return allGlyphsHintList 298 | 299 | 300 | def run(font, parentDir, coord_option): 301 | kTTHintsFileName = "tthints" 302 | 303 | tthintsFilePath = os.path.join(parentDir, kTTHintsFileName) 304 | selectedGlyphs = [ 305 | font[gIndex].name for gIndex in range(len(font)) 306 | if fl.Selected(gIndex)] 307 | 308 | if not len(selectedGlyphs): 309 | print "Select the glyph(s) to process and try again." 310 | return 311 | 312 | if os.path.exists(tthintsFilePath): 313 | print "WARNING: The %s file at %s will be modified." % ( 314 | kTTHintsFileName, tthintsFilePath) 315 | ttHintsList = readTTHintsFile(tthintsFilePath) 316 | modFile = True 317 | else: 318 | ttHintsList = [] 319 | modFile = False 320 | 321 | if len(ttHintsList): 322 | storedGlyphs = processTTHintsFileData(ttHintsList)[0] 323 | ttHintsDict = processTTHintsFileData(ttHintsList)[1] 324 | else: 325 | storedGlyphs = [] 326 | ttHintsDict = {} 327 | 328 | allGlyphsHintList = processGlyphs( 329 | selectedGlyphs, storedGlyphs, ttHintsDict, coord_option) 330 | 331 | if len(allGlyphsHintList) > 1: 332 | writeTTHintsFile(allGlyphsHintList, tthintsFilePath) 333 | else: 334 | print "The script found no TrueType hints to output." 335 | return 336 | 337 | if not modFile: 338 | print "File %s was saved at %s" % (kTTHintsFileName, tthintsFilePath) 339 | 340 | print "Done!" 341 | 342 | 343 | def preRun(coord_option=False): 344 | # Clear the Output window 345 | fl.output = '\n' 346 | 347 | if fl.count == 0: 348 | print "Open a font first." 349 | return 350 | 351 | font = fl.font 352 | 353 | if len(font) == 0: 354 | print "The font has no glyphs." 355 | return 356 | 357 | try: 358 | parentDir = os.path.dirname(os.path.realpath(font.file_name)) 359 | except AttributeError: 360 | print "The font has not been saved. Please save the font and try again." 361 | return 362 | 363 | run(font, parentDir, coord_option) 364 | 365 | 366 | if __name__ == "__main__": 367 | preRun() 368 | -------------------------------------------------------------------------------- /TrueType/tthDupe_coords.py: -------------------------------------------------------------------------------- 1 | #FLM: TT Hints Duplicator_coords 2 | # coding: utf-8 3 | 4 | import os 5 | import sys 6 | 7 | __copyright__ = __license__ = """ 8 | Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the "Software"), 12 | to deal in the Software without restriction, including without limitation 13 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 | and/or sell copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | """ 28 | 29 | __doc__ = u''' 30 | TT Hints Duplicator_coords 31 | 32 | This script was written to duplicate TT hinting data across compatible styles 33 | of a typeface family, cutting the time needed for TT hinting by a significant 34 | amount. The script is run as a FontLab macro, and does not need any of the 35 | involved fonts to be open. 36 | 37 | The script duplicates `tthints` files by reading information from the source 38 | `tthints` file and associated fonts, and comparing this data to the target 39 | fonts. It will not modify source- or target fonts in any way. 40 | 41 | The script is smart enough to not re-process the source folder, so it is safe 42 | to pick the root of a font project as the target directory. 43 | 44 | This script imports the `TT Hints Duplicator` script, therefore needs to be 45 | run from the same folder. 46 | 47 | Note: 48 | 1) 49 | This script will not process Delta hints. If Delta hints are present in a 50 | glyph, an error message will be output, and the Delta hints omitted from the 51 | output `tthints` file. 52 | 53 | 2) 54 | The script can only process TT instructions that are attached to *on-curve* 55 | points, because those are the only ones that will have the same coordinates 56 | in both PS and TT outlines. If there are hints attached to off-curve points, 57 | the whole glyph will be omitted from the output `tthints` file. 58 | 59 | 3) 60 | It is expected that overlaps are removed in the source CFF and TTF files. 61 | This ensures outline predictability. 62 | Depending on the drawing it can mean that there is some work to be done for 63 | compatibilizing the outlines across the style, which is usually less work 64 | than re-hinting. 65 | 66 | 4) 67 | Duplicating horizontal sidebearing-hints is not supported at this time. 68 | 69 | 70 | ================================================== 71 | Versions: 72 | 73 | v1.0 - Apr 18 2015 - First public release. 74 | ''' 75 | 76 | 77 | def findFile(fileName, path): 78 | 'Find file of given fileName, starting at path.' 79 | for root, dirs, files in os.walk(path): 80 | if fileName in files: 81 | return os.path.join(root) 82 | else: 83 | return None 84 | 85 | 86 | moduleName = 'tthDupe.py' 87 | userFolder = os.path.expanduser('~') 88 | customModulePathMAC = os.path.join( 89 | userFolder, 'Library', 'Application Support', 90 | 'FontLab', 'Studio 5', 'Macros') 91 | customModulePathPC = os.path.join( 92 | userFolder, 'Documents', 'FontLab', 'Studio5', 'Macros') 93 | customModulePathMAC = os.path.expanduser(customModulePathMAC) 94 | customModulePathPC = os.path.expanduser(customModulePathPC) 95 | possibleModulePaths = [fl.userpath, customModulePathMAC, customModulePathPC] 96 | 97 | print '\nLooking for %s ... ' % (moduleName) 98 | for path in possibleModulePaths: 99 | modPath = findFile(moduleName, path) 100 | if modPath: 101 | print 'found at %s' % modPath 102 | break 103 | 104 | if not modPath: 105 | # Module was not found. World ends. 106 | print '\ 107 | Not found in the following folders:\n%s\n\ 108 | Please make sure the possibleModulePaths list in this script \ 109 | points to a folder containing %s' % ('\n'.join(possibleModulePaths), moduleName) 110 | 111 | else: 112 | # Module was found, import it and run it. 113 | if modPath not in sys.path: 114 | sys.path.append(modPath) 115 | 116 | import tthDupe 117 | tthDupe.run(writeCoordinates=True) 118 | -------------------------------------------------------------------------------- /Type1/OutlineCheckDialog.py: -------------------------------------------------------------------------------- 1 | #FLM: Outline Check 2 | # Reports on, and optionionally attempts to fix possible outline problems 3 | __copyright__ = """ 4 | Copyright 2014 Adobe Systems Incorporated (http://www.adobe.com/). All Rights Reserved. 5 | This software is licensed as OpenSource, under the Apache License, Version 2.0. This license is available at: http://opensource.org/licenses/Apache-2.0. 6 | """ 7 | 8 | __doc__ = """ 9 | CheckOutlines v1.9 Oct 20 2014 10 | 11 | This script will apply the Adobe 'focus' checks to the specified glyphs. 12 | It can both report errors, and attempt to fix some of them. 13 | 14 | You should always run this script with the "Check Overlaps" and "Check 15 | Coincident Paths" options turned on. If present, these are always 16 | errors, and this library will fix them reliably. However, you should 17 | always check the changes; you may prefer to fix them differently. 18 | 19 | If you ask that problems be fixed, the report will note which issues are fixed 20 | by appending "Done" to the error message line. 21 | 22 | The other tests are useful, but the issues reported can be present 23 | intentionally in some fonts. Woodcuts and other fonts with short paths 24 | and sharp angles can generate many messages. Also, you should 25 | allow this tool to fix these problems just one glyph at a time, and make 26 | sure you like the changes: the fixes are far less reliable, and will in 27 | some cases mangle the glyph. 28 | 29 | Inspect Smoothness. Default off. This test checks that two curve 30 | segments come together such that there is no angle at the join. 31 | Another way of saying this is that the two bezier control points 32 | that control the angle of the join lie on the same straight line 33 | through the node. It is rare that you would want an angle where two 34 | curves meet. Fixing this will usually make the curve look better 35 | 36 | Inspect Sharp Angles/Spikes. Default off. Very sharp angles will cause 37 | strange black spikes at large point sizes in PostScript 38 | interpreters. These happen at very specific and unpredictable point 39 | sizes, so the problem cannot be seen by trying just a few integer 40 | point sizes. The tool will fix this by blunting the point of the 41 | angle by cutting off the tip and substituting a very short line. 42 | 43 | Inspect Small Triangles. Default off. This test verifies that both 44 | bezier control points lie on the same side of the curve segment they 45 | control, and that they do not intersect. Following this rule makes 46 | hinting work better: fewer pixel pop-outs and dents at small point 47 | sizes. 48 | 49 | Inspect Path Direction. Default is check on, Bad path orientation and 50 | overlap will make the glyph fill with black incorrectly at some 51 | point sizes. 52 | 53 | Do All Inspection Tests: This includes: 54 | 55 | Check that path has more than 2 nodes. This is always a mistake; either 56 | the path has no internal area, or it has no points at the extremes. 57 | If the path has no internal area, then the tool will delete it when 58 | fixing problems. The tool reports: 59 | "Warning: Subpath with only 2 graphic elements at ..." 60 | 61 | Check that two successive points are not on top of each other. Always a 62 | mistake. The tool reports: 63 | "Need to remove zero-length element" 64 | 65 | Check for sharp angles. See "-x" above. 66 | 67 | Check that that a curve does not form a loop. This is always 68 | unintentional, and PS interpreters really don't like it. The tool 69 | reports: 70 | "Need to inspect for possible loop/inflection: " 71 | 72 | Check if ""Need to fix control point outside of endpoints..." This tests 73 | that the control points are inside the safe region for drawing. Draw 74 | a line between the two end-points of a curve. Then draw a 75 | perpendicular line from each end-point towards the outside of the 76 | curve. The control points should lie within the region defined by the 77 | three lines. If not, then at best there is one or more vertical or 78 | horizontal extremes that do not have a node, and these will cause 79 | one-pixel pop-outs or dents in the outline at small point sizes. At 80 | worst, you have accidentally created a control point that points back in 81 | the direction from which the curve came, and you get an ugly spike 82 | at large point sizes. The tool does NOT fix these. 83 | 84 | Check for two successive line segments that could be a single line. Has 85 | no effect on rasterization, but makes the glyph more complicated to 86 | edit. 87 | 88 | Check for a curve that could be a line. If not perfectly flat, this 89 | cause pixel pop-outs or dents that would not otherwise happen. 90 | 91 | Check if the curve is smooth: see test "-s" above. 92 | 93 | Do triangle test: see option -3 above. 94 | 95 | 96 | Tolerance for colinear lines (default 0.01 units). If two successive 97 | staright lines deviate from being parallel by less then this 98 | tolerance, then you need only one line segment instead of two. Fix 99 | these to make the font a little smaller. 100 | 101 | Tolerance for arclength-similarity (default 0.055 units). If two 102 | succeeding curves look alike within this tolerance, you can combine 103 | them into a single curve. 104 | 105 | Tolerance for coincident paths (default 0.909 coverage) If two lines lie 106 | within this tolerance, then they will look like they are on top of 107 | each other. This is almost always a mistake in drawing. 108 | 109 | """ 110 | 111 | error_doc = """ 112 | The following is list of the checkOutlines error messages, and a brief 113 | explanation of each. The error messages here may be just the first part 114 | of the actual message, with additional info and cordinates given in the 115 | rest of the actual message. 116 | 117 | "Near-miss on orthogonal transition between (path segment1) and 118 | (pathsegment2)" 119 | These two path segments are so nearly at right angles, that I 120 | suspect that they were intended to be a right angle. 121 | 122 | "Need to delete incomplete subpath at (path segment)" 123 | This contour doesn't have enough points to make a closed path 124 | 125 | Need to delete orphan subpath with moveto at (path segment)" 126 | This contour is outside the font BBox, and is therefore was probably 127 | drawn accidentally. 128 | 129 | "May need to insert extremum in element (path segment)" 130 | Looks like the path element contains a horizontal or vertical 131 | extreme; if you want this hinted, you need a control point there. 132 | 133 | "Need to remove double-sharp element" 134 | "Need to clip sharp angle between two path segments)" 135 | "Extremely sharp angle between (two path segments)" 136 | Very sharp angles can cause PostScript interpreters to draw long 137 | spikes. Best to blunt these with a short line-segment across the end 138 | of the angle.. 139 | 140 | "Need to convert straight curve" 141 | This path is a curve type, but is straight - might as well make it a 142 | line. 143 | 144 | "Need to delete tiny subpath at (path segment)" 145 | This contour is so small, it must be have been drawn accidentally. 146 | 147 | "Need to fix coincident control points" 148 | Two control points lie on top of each other. This can act like an 149 | intersection. 150 | 151 | "Need to fix control point outside of endpoints" 152 | "Need to fix point(s) outside triangle by %d units in X and %d units in Y" 153 | The Bezier control points for a curve segment must lie on the same 154 | side of the curve segment, and within lines orthogonal to the curve 155 | segment at the end points. When this is not the case, there are 156 | vertical and horizontal extremes with no control points. This is 157 | often the result of an accident while drawing. 158 | 159 | "Need to fix wrong orientation on subpath with original moveto at 160 | Direction of path is wrong in this contour. 161 | 162 | "Need to inspect coincident paths: with original moveto at ..." 163 | Two line or curve segments either coincide, or are so close they 164 | will look like they coincide. 165 | 166 | "Need to inspect for possible loop/inflection (path segment)" 167 | This looks like a single path that makes a loop. If so, it is very 168 | bad for rasterizers. 169 | 170 | "Need to inspect unsmoothed transition between (two path segments)" 171 | There is an angle between two path segments which is so shallow that 172 | it was probably meant to be smooth. 173 | 174 | "Need to join colinear lines" 175 | Two straight lines are lined up, or very nearly so. These 176 | should be replaced by a single line segment 177 | 178 | "Need to remove one-unit closepath" 179 | A one-unit-long line is silly. Make the previous segment extend to 180 | the end point. No important consequences. 181 | 182 | "Need to remove zero-length element" 183 | Two neighboring control points lie on top of each other. This 184 | confuses rasterizers. Get rid of one. 185 | 186 | "NOTE: (number) intersections found. Please inspect." 187 | Two paths cross. This must be fixed. 188 | 189 | "Outline's bounding-box (X:%d %d Y:%d %d) looks too large." 190 | Bounding box of this contour exceeds the font-bounding box, so there 191 | is probably some error. 192 | 193 | "Warning: path #%d with original moveto at %d %d has no extreme-points 194 | on its curveto's. Does it need to be curvefit? Please inspect", 195 | This contour does not have control points at all of its vertical and 196 | horizontal extremes. 197 | 198 | "Warning: Subpath with only 2 graphic elements at (path segment)" 199 | This contour has only two nodes; it is either two superimposed line 200 | segments, or at least one curve without any point at extremes. 201 | """ 202 | 203 | import string 204 | import sys 205 | import os 206 | import re 207 | from FL import * 208 | try: 209 | import BezChar 210 | from AdobeFontLabUtils import Reporter, setFDKToolsPath, checkControlKeyPress, checkShiftKeyPress 211 | except ImportError,e: 212 | print "Failed to find the Adobe FDK support scripts AdobeFontLabUtils.py and BezChar.py." 213 | print "Please run the script FDK/Tools/FontLab/installFontLabMacros.py script, and try again." 214 | print " Current directory:", os.path.abspath(os.getcwd()) 215 | print "Current list of search paths for modules:" 216 | import pprint 217 | pprint.pprint(sys.path) 218 | raise e 219 | import warnings 220 | warnings.simplefilter("ignore", RuntimeWarning) # supress waring about use of os.tempnam(). 221 | 222 | 223 | if os.name == "nt": 224 | coPath = setFDKToolsPath("checkoutlinesexe.exe") 225 | if not coPath: 226 | coPath = setFDKToolsPath("outlineCheck.exe") 227 | 228 | else: 229 | coPath = setFDKToolsPath("checkoutlinesexe") 230 | if not coPath: 231 | coPath = setFDKToolsPath("outlineCheck") 232 | 233 | 234 | 235 | gLogReporter = None # log file class instance. 236 | debug = 0 237 | kProgressBarThreshold = 8 # I bother witha progress bar just so the user can easily cancel without using CTRL-C 238 | kProgressBarTickStep = 4 239 | logFileName = "CheckOutlines.log" # Is written to "log" subdirectory from current font. 240 | kPrefsName = "CheckOutline.prefs" 241 | 242 | def reportCB(*args): 243 | for arg in args: 244 | print "\t\t" + str(arg) 245 | 246 | def logMsg(*args): 247 | global gLogReporter 248 | # used for printing output to console as well as log file. 249 | if not gLogReporter: 250 | for arg in args: 251 | print arg 252 | else: 253 | if gLogReporter.file: 254 | gLogReporter.write(*args) 255 | else: 256 | for arg in args: 257 | print arg 258 | gLogReporter = None 259 | 260 | 261 | def debugLogMsg(*args): 262 | if debug: 263 | logMsg(*args) 264 | 265 | 266 | class CheckOutlineOptions: 267 | # Holds the options for the module. 268 | # The values of all member items NOT prefixed with "_" are written to/read from 269 | # a preferences file. 270 | # This also gets/sets the same member fields in the passed object. 271 | def __init__(self): 272 | # Selection control fields 273 | self.CmdList1 = ["doWholeFamily", "doAllOpenFonts", "doCurrentFont"] 274 | self.CmdList2 = ["doAllGlyphs", "doSelectedGlyphs"] 275 | self.doWholeFamily = 0 276 | self.doAllOpenFonts = 0 277 | self.doCurrentFont = 1 278 | self.doAllGlyphs = 0 279 | self.doSelectedGlyphs = 1 280 | 281 | # Test control fields 282 | self.beVerbose = 0 283 | self.doOverlapCheck = 1 284 | self.doCoincidentPathTest = 1 285 | self.skipInspectionTests = 1 286 | self.doAllInspectionTests = 0 287 | self.doSmoothnessTest = 0 288 | self.doSpikeTest = 0 289 | self.doTriangleTest = 0 290 | self.doPathDirectionTest = 1 291 | self.doFixProblems = 0 292 | self.curveTolerance = "" 293 | self.lineTolerance = "" 294 | self.pathTolerance = "" 295 | self.debug = 0 296 | 297 | # items not written to prefs 298 | self._prefsBaseName = kPrefsName 299 | self._prefsPath = None 300 | 301 | def _getPrefs(self, callerObject = None): 302 | foundPrefsFile = 0 303 | 304 | # We will put the prefs file in a directory "Preferences" at the same level as the Macros directory 305 | dirPath = os.path.dirname(BezChar.__file__) 306 | name = " " 307 | while name and (name.lower() != "macros"): 308 | name = os.path.basename(dirPath) 309 | dirPath = os.path.dirname(dirPath) 310 | if name.lower() != "macros" : 311 | dirPath = None 312 | 313 | if dirPath: 314 | dirPath = os.path.join(dirPath, "Preferences") 315 | if not os.path.exists(dirPath): # create it so we can save a prefs file there later. 316 | try: 317 | os.mkdir(dirPath) 318 | except (IOError,OSError): 319 | logMsg("Failed to create prefs directory %s" % (dirPath)) 320 | return foundPrefsFile 321 | else: 322 | return foundPrefsFile 323 | 324 | # the prefs directory exists. Try and open the file. 325 | self._prefsPath = os.path.join(dirPath, self._prefsBaseName) 326 | if os.path.exists(self._prefsPath): 327 | try: 328 | pf = file(self._prefsPath, "rt") 329 | data = pf.read() 330 | prefs = eval(data) 331 | pf.close() 332 | except (IOError, OSError): 333 | logMsg("Prefs file exists but cannot be read %s" % (self._prefsPath)) 334 | return foundPrefsFile 335 | 336 | # We've successfully read the prefs file 337 | foundPrefsFile = 1 338 | kelList = prefs.keys() 339 | for key in kelList: 340 | exec("self.%s = prefs[\"%s\"]" % (key,key)) 341 | 342 | # Add/set the memebr fields of the calling object 343 | if callerObject: 344 | keyList = dir(self) 345 | for key in keyList: 346 | if key[0] == "_": 347 | continue 348 | exec("callerObject.%s = self.%s" % (key, key)) 349 | 350 | return foundPrefsFile 351 | 352 | 353 | def _savePrefs(self, callerObject = None): 354 | prefs = {} 355 | if not self._prefsPath: 356 | return 357 | 358 | keyList = dir(self) 359 | for key in keyList: 360 | if key[0] == "_": 361 | continue 362 | if callerObject: 363 | exec("self.%s = callerObject.%s" % (key, key)) 364 | exec("prefs[\"%s\"] = self.%s" % (key, key)) 365 | try: 366 | pf = file(self._prefsPath, "wt") 367 | pf.write(repr(prefs)) 368 | pf.close() 369 | logMsg("Saved prefs in %s." % self._prefsPath) 370 | except (IOError, OSError): 371 | logMsg("Failed to write prefs file in %s." % self._prefsPath) 372 | 373 | def doCheck(options): 374 | global gLogReporter 375 | arg_string = "" 376 | if options.doFixProblems: 377 | arg_string = arg_string + " -n" 378 | if options.beVerbose: 379 | arg_string = arg_string + " -v" 380 | if options.skipInspectionTests: 381 | arg_string = arg_string + " -I" 382 | if options.doSmoothnessTest: 383 | arg_string = arg_string + " -s" 384 | if options.doSpikeTest: 385 | arg_string = arg_string + " -x" 386 | if options.doTriangleTest: 387 | arg_string = arg_string + " -3" 388 | if not options.doPathDirectionTest: 389 | arg_string = arg_string + " -O" 390 | if not options.doOverlapCheck: 391 | arg_string = arg_string + " -V" 392 | if not options.doCoincidentPathTest: 393 | arg_string = arg_string + " -k" 394 | if options.curveTolerance: 395 | arg_string = arg_string + " -C " + str(options.curveTolerance) 396 | if options.lineTolerance: 397 | arg_string = arg_string + " -L " + str(options.lineTolerance) 398 | if options.pathTolerance: 399 | arg_string = arg_string + " -K " + str(options.pathTolerance) 400 | 401 | if options.doAllOpenFonts: 402 | numOpenFonts = fl.count 403 | elif options.doWholeFamily: 404 | # close all fonts not in the family; open the ones that are missing. 405 | numOpenFonts = fl.count 406 | elif options.doCurrentFont: 407 | numOpenFonts = 1 408 | else: 409 | print "Error: unsupported option for font selection." 410 | 411 | fileName = fl.font.file_name 412 | if not fileName: 413 | fileName = "Font-Undefined" 414 | logDir = os.path.dirname(fileName) 415 | gLogReporter = Reporter(os.path.join(logDir, logFileName)) 416 | 417 | tempBaseName = os.tempnam() 418 | options.tempBez = tempBaseName + ".bez" 419 | 420 | # set up progress bar 421 | if options.doAllGlyphs: 422 | numGlyphs = len(fl.font) 423 | else: 424 | numGlyphs = fl.count_selected 425 | numProcessedGlyphs = numGlyphs * numOpenFonts 426 | if numProcessedGlyphs > kProgressBarThreshold: 427 | fl.BeginProgress("Checking glyphs...", numProcessedGlyphs) 428 | tick = 0 429 | if options.doAllGlyphs: 430 | for fi in range(numOpenFonts): 431 | if numOpenFonts == 1: 432 | font = fl.font 433 | else: 434 | font = fl[fi] 435 | 436 | logMsg("Checking %s." % (font.font_name)) 437 | lenFont = len(font) 438 | for gi in range(lenFont): 439 | glyph = font[gi] 440 | CheckGlyph(options, glyph, arg_string) 441 | if numGlyphs > kProgressBarThreshold: 442 | tick = tick + 1 443 | if (tick % kProgressBarTickStep == 0): 444 | result = fl.TickProgress(tick) 445 | if not result: 446 | break 447 | CheckGlyph(options, glyph, arg_string) 448 | 449 | elif options.doSelectedGlyphs: 450 | if numOpenFonts == 1: # we can use the current selection 451 | font = fl.font 452 | lenFont = len(font) 453 | logMsg("Checking %s." % (font.font_name)) 454 | for gi in range(lenFont): 455 | if fl.Selected(gi): 456 | glyph = font[gi] 457 | if numGlyphs > kProgressBarThreshold: 458 | tick = tick + 1 459 | if (tick % kProgressBarTickStep == 0): 460 | result = fl.TickProgress(tick) 461 | if not result: 462 | break 463 | CheckGlyph(options, glyph, arg_string) 464 | 465 | else: # we can't assume that GI's are the same in every font. 466 | # Collect the selected glyph names from the current font, 467 | # and then index in all fonts by name. 468 | nameList = [] 469 | for gi in range(lenFont): 470 | if fl.Selected(gi): 471 | nameList.append(fl.font[gi].name) 472 | for fi in range(numOpenFonts): 473 | font = fl[fi] 474 | logMsg("Checking %s." % (font.font_name)) 475 | lenFont = len(font) 476 | for gname in nameList: 477 | gi = font.FindGlyph(gname) 478 | if gi > -1: 479 | glyph = font[gi] 480 | if numGlyphs > kProgressBarThreshold: 481 | tick = tick + 1 482 | if (tick % kProgressBarTickStep == 0): 483 | result = fl.TickProgress(tick) 484 | if not result: 485 | break 486 | CheckGlyph(options, glyph, arg_string) 487 | else: 488 | line = "Glyph in not in font" 489 | logMsg("\t" + line + os.linesep) 490 | 491 | else: 492 | print "Error: unsupported option for glyph selection." 493 | gLogReporter.close() 494 | if numGlyphs > kProgressBarThreshold: 495 | fl.EndProgress() 496 | 497 | gLogReporter = None 498 | # end of doCheck 499 | 500 | def CheckGlyph(options, flGlyph, arg_string): 501 | logMsg("\tglyph %s." % flGlyph.name) 502 | numPaths = flGlyph.GetContoursNumber() 503 | numLayers = flGlyph.layers_number 504 | mastersNodes = [] 505 | if numLayers == 0: 506 | numLayers = 1 # allow for old FontLab variation. 507 | changedGlyph = 0 508 | for layer in range(numLayers): 509 | try: 510 | bezData = BezChar.ConvertFLGlyphToBez(flGlyph, layer) 511 | except SyntaxError,e: 512 | logMsg(e) 513 | logMsg("Skipping glyph %s." % flGlyph.name) 514 | bp = open(options.tempBez, "wt") 515 | bp.write(bezData) 516 | bp.close() 517 | command = "%s -o %s %s 2>&1" % (coPath, arg_string, options.tempBez) 518 | p = os.popen(command) 519 | log = p.read() 520 | myLength = len(log) 521 | p.close() 522 | if log: 523 | msg = log 524 | logMsg( msg) 525 | if options.debug: 526 | print options.tempBez 527 | print command 528 | print log 529 | 530 | if options.doFixProblems and (myLength > 0): 531 | if os.path.exists(options.tempBez): 532 | bp = open(options.tempBez, "rt") 533 | newBezData = bp.read() 534 | bp.close() 535 | if not options.debug: 536 | os.remove(options.tempBez) 537 | else: 538 | newBezData = None 539 | msg = "Skipping glyph %s. Failure in processing outline data" % (flGlyph.name) 540 | logMsg( msg) 541 | return 542 | changedGlyph = 1 543 | nodes = BezChar.MakeGlyphNodesFromBez(flGlyph.name, newBezData) 544 | mastersNodes.append(nodes) 545 | 546 | if changedGlyph: 547 | flGlyph.RemoveHints(1) 548 | flGlyph.RemoveHints(2) 549 | flGlyph.Clear() 550 | if numLayers == 1: 551 | flGlyph.Insert(nodes, 0) 552 | else: 553 | # make sure we didn't end up with different node lists when working with MM designs. 554 | nlen = len(mastersNodes[0]) 555 | numMasters = len(mastersNodes) 556 | for list in mastersNodes[1:]: 557 | if nlen != len(list): 558 | logMsg("Error: node lists after fixup are not same length in all masters, Skipping %s." % flGlyph.name) 559 | raise ACError 560 | flGlyph.Insert(nodes, 0) 561 | 562 | for i in range(nlen): 563 | mmNode = flGlyph[i] 564 | points = mmNode.points 565 | mmNode.type = mastersNodes[0][i].type 566 | for j in range(numMasters): 567 | targetPointList = mmNode.Layer(j) 568 | smNode = mastersNodes[j][i] 569 | srcPointList = smNode.points 570 | for pi in range(mmNode.count): 571 | targetPointList[pi].x = srcPointList[pi].x 572 | targetPointList[pi].y = srcPointList[pi].y 573 | return 574 | 575 | 576 | class OutlineCheckDialog: 577 | def __init__(self): 578 | dWidth = 500 579 | dMargin = 25 580 | 581 | # Selection control positions 582 | xMax = dWidth - dMargin 583 | y0 = dMargin + 20 584 | x1 = dMargin + 20 585 | x2 = dMargin + 300 586 | y1 = y0 + 30 587 | y2 = y1 + 30 588 | lastSelectionY = y2+40 589 | 590 | # Test control positions 591 | yTopFrame = lastSelectionY + 10 592 | yt1= yTopFrame + 20 593 | xt1 = dMargin + 20 594 | buttonSpace = 20 595 | toleranceWidth = 50 596 | toleranceHeight = 25 597 | xt2 = xt1 598 | yt2 = yt1 + buttonSpace 599 | xt3 = xt2 600 | yt3 = yt2 + buttonSpace 601 | xt4 = xt3 602 | yt4 = yt3 + buttonSpace 603 | xt5 = xt4 604 | yt5 = yt4 + buttonSpace 605 | xt6 = xt5 606 | yt6 = yt5 + buttonSpace 607 | xt7 = xt6 608 | yt7 = yt6 + buttonSpace 609 | xt8 = xt7 610 | yt8 = yt7 + buttonSpace 611 | xt9 = xt8 612 | yt9 = yt8 + buttonSpace 613 | xt10 = xt9 614 | yt10 = yt9 + buttonSpace 615 | lastButtonX = xt10 616 | lastButtony = yt10 617 | # tolerance values 618 | xt11 = lastButtonX + 100 619 | yt11 = lastButtony + toleranceHeight + 5 620 | xt12 = xt11 621 | yt12 = yt11 + toleranceHeight + 5 622 | xt13 = xt11 623 | yt13 = yt12 + toleranceHeight + 5 624 | 625 | lastY = yt13 + 40 626 | 627 | dHeight = lastY + 50 628 | xt14 = lastButtonX 629 | yt14 = dHeight - 35 630 | 631 | self.d = Dialog(self) 632 | self.d.size = Point(dWidth, dHeight) 633 | self.d.Center() 634 | self.d.title = "Outline Checker" 635 | self.options = CheckOutlineOptions() 636 | self.options._getPrefs(self) # This both loads prefs and assigns the member fields of the dialog. 637 | options.debug = debug 638 | 639 | 640 | # no font warning. 641 | self.d.AddControl(STATICCONTROL, Rect(aIDENT, aIDENT, aIDENT, aIDENT), "frame", STYLE_FRAME) 642 | 643 | if fl.font == None: 644 | self.d.AddControl(STATICCONTROL, Rect(aIDENT2, aIDENT2, aAUTO, aAUTO), "label", STYLE_LABEL, "Please first open a font.") 645 | return 646 | 647 | self.d.AddControl(STATICCONTROL, Rect(dMargin, dMargin, xMax, lastSelectionY), "frame", STYLE_FRAME, "Glyph Selection") 648 | 649 | self.d.AddControl(CHECKBOXCONTROL, Rect(x1, y1, x1+200, aAUTO), "doAllGlyphs", STYLE_CHECKBOX, "Process all glyphs") 650 | 651 | self.d.AddControl(CHECKBOXCONTROL, Rect(x1, y2, x1+200, aAUTO), "doSelectedGlyphs", STYLE_CHECKBOX, "Process selected glyphs") 652 | 653 | self.d.AddControl(CHECKBOXCONTROL, Rect(x2, y0, x2 + 200, aAUTO), "doCurrentFont", STYLE_CHECKBOX, "Current Font") 654 | 655 | self.d.AddControl(CHECKBOXCONTROL, Rect(x2, y1, x2 + 200, aAUTO), "doAllOpenFonts", STYLE_CHECKBOX, "All open fonts") 656 | 657 | self.d.AddControl(CHECKBOXCONTROL, Rect(x2, y2, x2 + 200, aAUTO), "doWholeFamily", STYLE_CHECKBOX, "All fonts in family") 658 | 659 | 660 | self.d.AddControl(STATICCONTROL, Rect(dMargin, yTopFrame, xMax, lastY), "frame2", STYLE_FRAME, "Test Selections") 661 | 662 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt1, yt1, xt1+200, aAUTO), "beVerbose", STYLE_CHECKBOX, "List checks done in log") 663 | 664 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt2, yt2, xt1+200, aAUTO), "doOverlapCheck", STYLE_CHECKBOX, "Check Overlaps") 665 | 666 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt3, yt3, xt1+200, aAUTO), "doCoincidentPathTest", STYLE_CHECKBOX, "Check Coincident Paths") 667 | 668 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt4, yt4, xt1+200, aAUTO), "skipInspectionTests", STYLE_CHECKBOX, "Skip All Inspection Tests") 669 | 670 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt5, yt5, xt1+200, aAUTO), "doAllInspectionTests", STYLE_CHECKBOX, "Do All Inspection Tests") 671 | 672 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt6, yt6, xt1+200, aAUTO), "doSmoothnessTest", STYLE_CHECKBOX, "Inspect Smoothness") 673 | 674 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt7, yt7, xt1+200, aAUTO), "doSpikeTest", STYLE_CHECKBOX, "Inspect Sharp Angles/Spikes") 675 | 676 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt8, yt8, xt1+200, aAUTO), "doTriangleTest", STYLE_CHECKBOX, "Inspect Small Triangles") 677 | 678 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt9, yt9, xt1+200, aAUTO), "doPathDirectionTest", STYLE_CHECKBOX, "Inspect Path Direction") 679 | 680 | self.d.AddControl(CHECKBOXCONTROL, Rect(xt10, yt10, xt1+300, aAUTO), "doFixProblems", STYLE_CHECKBOX, "Fix Problems (always save font before!)") 681 | 682 | self.d.AddControl(EDITCONTROL, Rect(xt11, yt11, xt11 +toleranceWidth, yt11 + toleranceHeight ), "curveTolerance", STYLE_EDIT, "default 0.125 units") 683 | self.d.AddControl(STATICCONTROL, Rect(xt11+toleranceWidth + 8, yt11+5, xt12+toleranceWidth + 8 + 200, yt11+ toleranceHeight ), "curveToleranceLabel", STYLE_LABEL, "tolerance for linear curves") 684 | 685 | self.d.AddControl(EDITCONTROL, Rect(xt12, yt12, xt12 +toleranceWidth, yt12 + toleranceHeight), "lineTolerance", STYLE_EDIT, "default 0.01 units") 686 | self.d.AddControl(STATICCONTROL, Rect(xt12+toleranceWidth + 8, yt12+5, xt12+toleranceWidth + 8 + 200, yt12 + toleranceHeight ), "lineToleranceLabel", STYLE_LABEL, "tolerance for colinear lines") 687 | 688 | self.d.AddControl(EDITCONTROL, Rect(xt13, yt13, xt13 +toleranceWidth, yt13 + toleranceHeight ), "pathTolerance", STYLE_EDIT, "default 0.909 units") 689 | self.d.AddControl(STATICCONTROL, Rect(xt13+toleranceWidth + 8, yt13+5,xt12+toleranceWidth + 8 + 200, yt13 + toleranceHeight ), "pathToleranceLabel", STYLE_LABEL, "tolerance for coincident paths") 690 | 691 | self.d.AddControl(BUTTONCONTROL, Rect(xt14, yt14, xt14+60, yt14+20), "help", STYLE_BUTTON, "Help") 692 | 693 | def toggleOffAllOtherSelections(self, cmd, cmdList): 694 | exec("curVal = self." + cmd) 695 | if curVal: 696 | curVal = 0 697 | else: 698 | curVal = 1 699 | for cmd_name in cmdList: 700 | if cmd_name == cmd: 701 | continue 702 | 703 | if cmd_name == "doGlyphString": 704 | self.doGlyphString = "" 705 | self.d.PutValue("doGlyphString") 706 | else: 707 | exec("self." + cmd_name + " = curVal") 708 | exec("self.d.PutValue(\"" + cmd_name + "\")") 709 | if curVal: 710 | break 711 | 712 | def on_doAllGlyphs(self, code): 713 | self.d.GetValue("doAllGlyphs") 714 | self.toggleOffAllOtherSelections("doAllGlyphs", self.CmdList2) 715 | 716 | def on_doSelectedGlyphs(self, code): 717 | self.d.GetValue("doSelectedGlyphs") 718 | self.toggleOffAllOtherSelections("doSelectedGlyphs", self.CmdList2) 719 | 720 | def on_doCurrentFont(self, code): 721 | self.d.GetValue("doCurrentFont") 722 | self.toggleOffAllOtherSelections("doCurrentFont", self.CmdList1) 723 | 724 | def on_doAllOpenFonts(self, code): 725 | self.d.GetValue("doAllOpenFonts") 726 | self.toggleOffAllOtherSelections("doAllOpenFonts", self.CmdList1) 727 | 728 | def on_doWholeFamily(self, code): 729 | self.d.GetValue("doWholeFamily") 730 | self.toggleOffAllOtherSelections("doWholeFamily", self.CmdList1) 731 | 732 | def on_doOverlapCheck(self, code): 733 | self.d.GetValue("doOverlapCheck") 734 | 735 | def on_skipInspectionTests(self, code): 736 | self.d.GetValue("skipInspectionTests") 737 | if self.skipInspectionTests: 738 | self.doSmoothnessTest = 0 739 | self.d.PutValue("doSmoothnessTest") 740 | self.doSpikeTest = 0 741 | self.d.PutValue("doSpikeTest") 742 | self.doTriangleTest = 0 743 | self.d.PutValue("doTriangleTest") 744 | else: 745 | # if one is on, don't turn on the others. 746 | if not (self.doSmoothnessTest or \ 747 | self.doSpikeTest or \ 748 | self.doTriangleTest): 749 | self.doSmoothnessTest = 1 750 | self.d.PutValue("doSmoothnessTest") 751 | self.doSpikeTest = 1 752 | self.d.PutValue("doSpikeTest") 753 | self.doTriangleTest = 1 754 | self.d.PutValue("doTriangleTest") 755 | 756 | def on_doSmoothnessTest(self, code): 757 | self.d.GetValue("doSmoothnessTest") 758 | if self.doSmoothnessTest and self.skipInspectionTests: 759 | self.skipInspectionTests = 0 760 | self.d.PutValue("skipInspectionTests") 761 | elif (not self.skipInspectionTests) and \ 762 | not (self.doSmoothnessTest or \ 763 | self.doSpikeTest or \ 764 | self.doTriangleTest): 765 | self.skipInspectionTests = 1 766 | self.d.PutValue("skipInspectionTests") 767 | 768 | 769 | def on_doSpikeTest(self, code): 770 | self.d.GetValue("doSpikeTest") 771 | if self.doSpikeTest and self.skipInspectionTests: 772 | self.skipInspectionTests = 0 773 | self.d.PutValue("skipInspectionTests") 774 | elif (not self.skipInspectionTests) and \ 775 | not (self.doSmoothnessTest or \ 776 | self.doSpikeTest or \ 777 | self.doTriangleTest): 778 | self.skipInspectionTests = 1 779 | self.d.PutValue("skipInspectionTests") 780 | 781 | def on_doTriangleTest(self, code): 782 | self.d.GetValue("doTriangleTest") 783 | if self.doTriangleTest and self.skipInspectionTests: 784 | self.skipInspectionTests = 0 785 | self.d.PutValue("skipInspectionTests") 786 | elif (not self.skipInspectionTests) and \ 787 | not (self.doSmoothnessTest or \ 788 | self.doSpikeTest or \ 789 | self.doTriangleTest): 790 | self.skipInspectionTests = 1 791 | self.d.PutValue("skipInspectionTests") 792 | 793 | def on_doPathDirectionTest(self, code): 794 | self.d.GetValue("doPathDirectionTest") 795 | 796 | def on_doCoincidentPathTest(self, code): 797 | self.d.GetValue("doCoincidentPathTest") 798 | 799 | def on_doFixProblems(self, code): 800 | self.d.GetValue("doFixProblems") 801 | 802 | def on_beVerbose(self, code): 803 | self.d.GetValue("beVerbose") 804 | 805 | def on_curveTolerance(self, code): 806 | self.d.GetValue("curveTolerance") 807 | 808 | def on_colinearLineTolerance(self, code): 809 | self.d.GetValue("lineTolerance") 810 | 811 | def on_curveTolerance(self, code): 812 | self.d.GetValue("pathTolerance") 813 | 814 | 815 | def on_ok(self, code): 816 | self.result = 1 817 | # update options 818 | self.options._savePrefs(self) # update prefs file 819 | 820 | def on_cancel(self, code): 821 | self.result = 0 822 | 823 | #OutlineCheckDialog 824 | def on_help(self, code): 825 | self.result = 2 826 | self.d.End() # only ok and cancel buttons do this automatically. 827 | 828 | def Run(self): 829 | self.d.Run() 830 | return self.result 831 | 832 | 833 | #OutlineCheckDialog 834 | class ACHelpDialog: 835 | def __init__(self): 836 | dWidth = 700 837 | dMargin = 25 838 | dHeight = 500 839 | self.result = 0 840 | 841 | self.d = Dialog(self) 842 | self.d.size = Point(dWidth, dHeight) 843 | self.d.Center() 844 | self.d.title = "Auto Hinting Help" 845 | 846 | editControl = self.d.AddControl(LISTCONTROL, Rect(dMargin, dMargin, dWidth-dMargin, dHeight-50), "helpText", STYLE_LIST, "") 847 | self.helpText = __doc__.splitlines() + error_doc.splitlines() 848 | self.helpText = map(lambda line: " " + line, self.helpText) 849 | self.d.PutValue("helpText") 850 | 851 | 852 | def on_cancel(self, code): 853 | self.result = 0 854 | 855 | def on_ok(self, code): 856 | self.result = 2 857 | 858 | def Run(self): 859 | self.d.Run() 860 | return self.result 861 | 862 | 863 | def run(): 864 | global debug 865 | 866 | dontShowDialog = 1 867 | if coPath: 868 | result = 2 869 | dontShowDialog = checkControlKeyPress() 870 | debug = not checkShiftKeyPress() 871 | 872 | if dontShowDialog: 873 | print "Hold down CONTROL key while starting this script in order to set options." 874 | if fl.count == 0: 875 | print "You must have a font open to run auto-hint" 876 | else: 877 | options = CheckOutlineOptions() 878 | options._getPrefs() # load current settings from prefs 879 | options.debug = debug 880 | doCheck(options) 881 | else: 882 | # I do this funky little loop so that control will return to the main dialgo after showing help. 883 | # Odd things happen to the dialog focus if you call the help dialgo directly from the main dialog. 884 | # in FontLab 4.6 885 | while result == 2: 886 | d = OutlineCheckDialog() 887 | result = d.Run() # returns 0 for cancel, 1 for ok, 2 for help 888 | if result == 1: 889 | options = CheckOutlineOptions() 890 | options._getPrefs() # load current settings from prefs 891 | options.debug = debug 892 | doCheck(options) 893 | elif result == 2: 894 | ACh = ACHelpDialog() 895 | result = ACh.Run() # returns 0 for cancel, 2 for ok 896 | 897 | if __name__ == '__main__': 898 | run() -------------------------------------------------------------------------------- /installFontLabMacros.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | __copyright__ = """ 4 | Copyright 2015-2016 Adobe Systems Incorporated (http://www.adobe.com/). All Rights Reserved. 5 | This software is licensed as OpenSource, under the Apache License, Version 2.0. This license is available at: http://opensource.org/licenses/Apache-2.0. */ 6 | """ 7 | 8 | __doc__ = """ 9 | v 2.0 Apr 11 2016 10 | 11 | Copies the folders under the parent directory of this script file 12 | to the appropriate place under FontLab program's Macros directory. 13 | """ 14 | 15 | import sys 16 | import re 17 | import os 18 | import shutil 19 | import traceback 20 | 21 | class InstallError(IOError): 22 | pass 23 | 24 | import stat 25 | kPermissions = stat.S_IWRITE | stat.S_IREAD | stat.S_IRGRP | stat.S_IWGRP | stat.S_IRUSR | stat.S_IWUSR 26 | 27 | def copyFile(srcPath, dstPath): 28 | if os.path.isfile(srcPath): 29 | try: 30 | shutil.copy(srcPath, dstPath) 31 | shutil.copystat(srcPath, dstPath) 32 | os.chmod(dstPath, kPermissions) 33 | except IOError: 34 | print "Failed to copy file %s to %s." % (srcPath, dstPath) 35 | print traceback.format_exception_only(sys.exc_type, sys.exc_value)[-1] 36 | print "Quitting - not all files were copied." 37 | raise InstallError 38 | print "Copied %s\n to %s\n" % (srcPath, dstPath) 39 | else: 40 | print "Failed to find src file '%s'." % (srcPath) 41 | 42 | 43 | def copyDir(srcDirPath, destDirPath): 44 | if not os.path.exists(destDirPath): 45 | os.mkdir(destDirPath) 46 | 47 | fileList = os.listdir(srcDirPath) 48 | for fileName in fileList: 49 | srcPath = os.path.join(srcDirPath, fileName) 50 | if os.path.isfile(srcPath): 51 | name,ext = os.path.splitext(fileName) 52 | if not ext in [".so", ".pyd", ".py"]: 53 | continue 54 | dstPath = os.path.join(destDirPath, fileName) 55 | copyFile(srcPath, dstPath) 56 | elif os.path.isdir(srcPath): 57 | newDestDirPath = os.path.join(destDirPath, fileName) 58 | copyDir(srcPath, newDestDirPath) 59 | else: 60 | print "Say what? this file is neither a dir nor a regular file:", srcPath 61 | 62 | def run(): 63 | # define where we will copy things 64 | if len(sys.argv) != 2: 65 | print "You must provide the path to FontLab's Macros folder." 66 | print "An example for Mac OSX:" 67 | print " python installFontLabMacros.py /Users//Library/Application\ Support/FontLab/Studio\ 5/Macros" 68 | return 69 | destBasePath = sys.argv[1] 70 | if not os.path.isdir(destBasePath): 71 | print "The path you supplied does not exist or is not a directory. Try again." 72 | return 73 | 74 | # get the path to the folder that contains this script 75 | srcBasePath = os.path.dirname(os.path.abspath(__file__)) 76 | 77 | # copy all folders and their contents to FontLab's Macros folder. 78 | # the Modules folder requires special handling 79 | dirList = os.listdir(srcBasePath) 80 | for dirName in dirList: 81 | if dirName.startswith('.'): 82 | continue 83 | srcDirPath = os.path.join(srcBasePath, dirName) 84 | if not os.path.isdir(srcDirPath): 85 | continue 86 | if dirName == "Modules": 87 | destDirPath = os.path.join(destBasePath, "System", dirName) 88 | else: 89 | destDirPath = os.path.join(destBasePath, dirName) 90 | copyDir(srcDirPath, destDirPath) 91 | 92 | 93 | if __name__ == "__main__": 94 | try: 95 | run() 96 | except InstallError: 97 | pass 98 | 99 | 100 | --------------------------------------------------------------------------------