1# Copyright 2014 Adobe. All rights reserved. 2 3""" 4This module supports using the Adobe FDK tools which operate on 'bez' 5files with UFO fonts. It provides low level utilities to manipulate UFO 6data without fully parsing and instantiating UFO objects, and without 7requiring that the AFDKO contain the robofab libraries. 8 9Modified in Nov 2014, when AFDKO added the robofab libraries. It can now 10be used with UFO fonts only to support the hash mechanism. 11 12Developed in order to support checkOutlines and autohint, the code 13supports two main functions: 14- convert between UFO GLIF and bez formats 15- keep a history of processing in a hash map, so that the (lengthy) 16processing by autohint and checkOutlines can be avoided if the glyph has 17already been processed, and the source data has not changed. 18 19The basic model is: 20 - read GLIF file 21 - transform GLIF XML element to bez file 22 - call FDK tool on bez file 23 - transform new bez file to GLIF XML element with new data, and save in list 24 25After all glyphs are done save all the new GLIF XML elements to GLIF 26files, and update the hash map. 27 28A complication in the Adobe UFO workflow comes from the fact we want to 29make sure that checkOutlines and autohint have been run on each glyph in 30a UFO font, when building an OTF font from the UFO font. We need to run 31checkOutlines, because we no longer remove overlaps from source UFO font 32data, because this can make revising a glyph much easier. We need to run 33autohint, because the glyphs must be hinted after checkOutlines is run, 34and in any case we want all glyphs to have been autohinted. The problem 35with this is that it can take a minute or two to run autohint or 36checkOutlines on a 2K-glyph font. The way we avoid this is to make and 37keep a hash of the source glyph drawing operators for each tool. When 38the tool is run, it calculates a hash of the source glyph, and compares 39it to the saved hash. If these are the same, the tool can skip the 40glyph. This saves a lot of time: if checkOutlines and autohint are run 41on all glyphs in a font, then a second pass is under 2 seconds. 42 43Another issue is that since we no longer remove overlaps from the source 44glyph files, checkOutlines must write any edited glyph data to a 45different layer in order to not destroy the source data. The ufoFont 46defines an Adobe-specific glyph layer for processed glyphs, named 47"glyphs.com.adobe.type.processedGlyphs". 48checkOutlines writes new glyph files to the processed glyphs layer only 49when it makes a change to the glyph data. 50 51When the autohint program is run, the ufoFont must be able to tell 52whether checkOutlines has been run and has altered a glyph: if so, the 53input file needs to be from the processed glyph layer, else it needs to 54be from the default glyph layer. 55 56The way the hashmap works is that we keep an entry for every glyph that 57has been processed, identified by a hash of its marking path data. Each 58entry contains: 59- a hash of the glyph point coordinates, from the default layer. 60This is set after a program has been run. 61- a history list: a list of the names of each program that has been run, 62 in order. 63- an editStatus flag. 64Altered GLIF data is always written to the Adobe processed glyph layer. The 65program may or may not have altered the outline data. For example, autohint 66adds private hint data, and adds names to points, but does not change any 67paths. 68 69If the stored hash for the glyph does not exist, the ufoFont lib will save the 70new hash in the hash map entry and will set the history list to contain just 71the current program. The program will read the glyph from the default layer. 72 73If the stored hash matches the hash for the current glyph file in the default 74layer, and the current program name is in the history list,then ufoFont 75will return "skip=1", and the calling program may skip the glyph. 76 77If the stored hash matches the hash for the current glyph file in the default 78layer, and the current program name is not in the history list, then the 79ufoFont will return "skip=0". If the font object field 'usedProcessedLayer' is 80set True, the program will read the glyph from the from the Adobe processed 81layer, if it exists, else it will always read from the default layer. 82 83If the hash differs between the hash map entry and the current glyph in the 84default layer, and usedProcessedLayer is False, then ufoFont will return 85"skip=0". If usedProcessedLayer is True, then the program will consult the 86list of required programs. If any of these are in the history list, then the 87program will report an error and return skip =1, else it will return skip=1. 88The program will then save the new hash in the hash map entry and reset the 89history list to contain just the current program. If the old and new hash 90match, but the program name is not in the history list, then the ufoFont will 91not skip the glyph, and will add the program name to the history list. 92 93 94The only tools using this are, at the moment, checkOutlines, checkOutlinesUFO 95and autohint. checkOutlines and checkOutlinesUFO use the hash map to skip 96processing only when being used to edit glyphs, not when reporting them. 97checkOutlines necessarily flattens any components in the source glyph file to 98actual outlines. autohint adds point names, and saves the hint data as a 99private data in the new GLIF file. 100 101autohint saves the hint data in the GLIF private data area, /lib/dict, 102as a key and data pair. See below for the format. 103 104autohint started with _hintFormat1_, a reasonably compact XML representation of 105the data. In Sep 2105, autohhint switched to _hintFormat2_ in order to be plist 106compatible. This was necessary in order to be compatible with the UFO spec, by 107was driven more immediately by the fact the the UFO font file normalization 108tools stripped out the _hintFormat1_ hint data as invalid elements. 109 110 111""" 112 113import ast 114import hashlib 115import logging 116import os 117import re 118import shutil 119 120from collections import OrderedDict 121from types import SimpleNamespace 122 123from fontTools.pens.basePen import BasePen 124from fontTools.pens.pointPen import AbstractPointPen 125from fontTools.ufoLib import UFOReader, UFOWriter 126from fontTools.ufoLib.errors import UFOLibError 127 128from . import fdTools, FontParseError 129 130 131log = logging.getLogger(__name__) 132 133_hintFormat1_ = """ 134 135Deprecated. See _hintFormat2_ below. 136 137A <hintset> element identifies a specific point by its name, and 138describes a new set of stem hints which should be applied before the 139specific point. 140 141A <flex> element identifies a specific point by its name. The point is 142the first point of a curve. The presence of the <flex> element is a 143processing suggestion, that the curve and its successor curve should 144be converted to a flex operator. 145 146One challenge in applying the hintset and flex elements is that in the 147GLIF format, there is no explicit start and end operator: the first path 148operator is both the end and the start of the path. I have chosen to 149convert this to T1 by taking the first path operator, and making it a 150move-to. I then also use it as the last path operator. An exception is a 151line-to; in T1, this is omitted, as it is implied by the need to close 152the path. Hence, if a hintset references the first operator, there is a 153potential ambiguity: should it be applied before the T1 move-to, or 154before the final T1 path operator? The logic here applies it before the 155move-to only. 156 157 <glyph> 158... 159 <lib> 160 <dict> 161 <key><com.adobe.type.autohint><key> 162 <data> 163 <hintSetList> 164 <hintset pointTag="point name"> 165 (<hstem pos="<decimal value>" width="decimal value" />)* 166 (<vstem pos="<decimal value>" width="decimal value" />)* 167 <!-- where n1-5 are decimal values --> 168 <hstem3 stem3List="n0,n1,n2,n3,n4,n5" />* 169 <!-- where n1-5 are decimal values --> 170 <vstem3 stem3List="n0,n1,n2,n3,n4,n5" />* 171 </hintset>* 172 (<hintSetList>* 173 (<hintset pointIndex="positive integer"> 174 (<stemindex>positive integer</stemindex>)+ 175 </hintset>)+ 176 </hintSetList>)* 177 <flexList> 178 <flex pointTag="point Name" /> 179 </flexList>* 180 </hintSetList> 181 </data> 182 </dict> 183 </lib> 184</glyph> 185 186Example from "B" in SourceCodePro-Regular 187<key><com.adobe.type.autohint><key> 188<data> 189<hintSetList id="64bf4987f05ced2a50195f971cd924984047eb1d79c8c43e6a0054f59cc85 190dea23a49deb20946a4ea84840534363f7a13cca31a81b1e7e33c832185173369086"> 191 <hintset pointTag="hintSet0000"> 192 <hstem pos="0" width="28" /> 193 <hstem pos="338" width="28" /> 194 <hstem pos="632" width="28" /> 195 <vstem pos="100" width="32" /> 196 <vstem pos="496" width="32" /> 197 </hintset> 198 <hintset pointTag="hintSet0005"> 199 <hstem pos="0" width="28" /> 200 <hstem pos="338" width="28" /> 201 <hstem pos="632" width="28" /> 202 <vstem pos="100" width="32" /> 203 <vstem pos="454" width="32" /> 204 <vstem pos="496" width="32" /> 205 </hintset> 206 <hintset pointTag="hintSet0016"> 207 <hstem pos="0" width="28" /> 208 <hstem pos="338" width="28" /> 209 <hstem pos="632" width="28" /> 210 <vstem pos="100" width="32" /> 211 <vstem pos="496" width="32" /> 212 </hintset> 213</hintSetList> 214</data> 215 216""" 217 218_hintFormat2_ = """ 219 220A <dict> element in the hintSetList array identifies a specific point by its 221name, and describes a new set of stem hints which should be applied before the 222specific point. 223 224A <string> element in the flexList identifies a specific point by its name. 225The point is the first point of a curve. The presence of the element is a 226processing suggestion, that the curve and its successor curve should be 227converted to a flex operator. 228 229One challenge in applying the hintSetList and flexList elements is that in 230the GLIF format, there is no explicit start and end operator: the first path 231operator is both the end and the start of the path. I have chosen to convert 232this to T1 by taking the first path operator, and making it a move-to. I then 233also use it as the last path operator. An exception is a line-to; in T1, this 234is omitted, as it is implied by the need to close the path. Hence, if a hintset 235references the first operator, there is a potential ambiguity: should it be 236applied before the T1 move-to, or before the final T1 path operator? The logic 237here applies it before the move-to only. 238 <glyph> 239... 240 <lib> 241 <dict> 242 <key><com.adobe.type.autohint></key> 243 <dict> 244 <key>id</key> 245 <string> <fingerprint for glyph> </string> 246 <key>hintSetList</key> 247 <array> 248 <dict> 249 <key>pointTag</key> 250 <string> <point name> </string> 251 <key>stems</key> 252 <array> 253 <string>hstem <position value> <width value></string>* 254 <string>vstem <position value> <width value></string>* 255 <string>hstem3 <position value 0>...<position value 5> 256 </string>* 257 <string>vstem3 <position value 0>...<position value 5> 258 </string>* 259 </array> 260 </dict>* 261 </array> 262 263 <key>flexList</key>* 264 <array> 265 <string><point name></string>+ 266 </array> 267 </dict> 268 </dict> 269 </lib> 270</glyph> 271 272Example from "B" in SourceCodePro-Regular 273<key><com.adobe.type.autohint><key> 274<dict> 275 <key>id</key> 276 <string>64bf4987f05ced2a50195f971cd924984047eb1d79c8c43e6a0054f59cc85dea23 277 a49deb20946a4ea84840534363f7a13cca31a81b1e7e33c832185173369086</string> 278 <key>hintSetList</key> 279 <array> 280 <dict> 281 <key>pointTag</key> 282 <string>hintSet0000</string> 283 <key>stems</key> 284 <array> 285 <string>hstem 338 28</string> 286 <string>hstem 632 28</string> 287 <string>hstem 100 32</string> 288 <string>hstem 496 32</string> 289 </array> 290 </dict> 291 <dict> 292 <key>pointTag</key> 293 <string>hintSet0005</string> 294 <key>stems</key> 295 <array> 296 <string>hstem 0 28</string> 297 <string>hstem 338 28</string> 298 <string>hstem 632 28</string> 299 <string>hstem 100 32</string> 300 <string>hstem 454 32</string> 301 <string>hstem 496 32</string> 302 </array> 303 </dict> 304 <dict> 305 <key>pointTag</key> 306 <string>hintSet0016</string> 307 <key>stems</key> 308 <array> 309 <string>hstem 0 28</string> 310 <string>hstem 338 28</string> 311 <string>hstem 632 28</string> 312 <string>hstem 100 32</string> 313 <string>hstem 496 32</string> 314 </array> 315 </dict> 316 </array> 317<dict> 318 319""" 320 321# UFO names 322PUBLIC_GLYPH_ORDER = "public.glyphOrder" 323 324ADOBE_DOMAIN_PREFIX = "com.adobe.type" 325 326PROCESSED_LAYER_NAME = "%s.processedglyphs" % ADOBE_DOMAIN_PREFIX 327PROCESSED_GLYPHS_DIRNAME = "glyphs.%s" % PROCESSED_LAYER_NAME 328 329HASHMAP_NAME = "%s.processedHashMap" % ADOBE_DOMAIN_PREFIX 330HASHMAP_VERSION_NAME = "hashMapVersion" 331HASHMAP_VERSION = (1, 0) # If major version differs, do not use. 332AUTOHINT_NAME = "autohint" 333CHECKOUTLINE_NAME = "checkOutlines" 334 335BASE_FLEX_NAME = "flexCurve" 336FLEX_INDEX_LIST_NAME = "flexList" 337HINT_DOMAIN_NAME1 = "com.adobe.type.autohint" 338HINT_DOMAIN_NAME2 = "com.adobe.type.autohint.v2" 339HINT_SET_LIST_NAME = "hintSetList" 340HSTEM3_NAME = "hstem3" 341HSTEM_NAME = "hstem" 342POINT_NAME = "name" 343POINT_TAG = "pointTag" 344STEMS_NAME = "stems" 345VSTEM3_NAME = "vstem3" 346VSTEM_NAME = "vstem" 347STACK_LIMIT = 46 348 349 350class BezParseError(ValueError): 351 pass 352 353 354class UFOFontData: 355 def __init__(self, path, log_only, write_to_default_layer): 356 self._reader = UFOReader(path, validate=False) 357 358 self.path = path 359 self._glyphmap = None 360 self._processed_layer_glyphmap = None 361 self.newGlyphMap = {} 362 self._fontInfo = None 363 self._glyphsets = {} 364 # If True, we are running in report mode and not doing any changes, so 365 # we skip the hash map and process all glyphs. 366 self.log_only = log_only 367 # Used to store the hash of glyph data of already processed glyphs. If 368 # the stored hash matches the calculated one, we skip the glyph. 369 self._hashmap = None 370 self.fontDict = None 371 self.hashMapChanged = False 372 # If True, then write data to the default layer 373 self.writeToDefaultLayer = write_to_default_layer 374 375 def getUnitsPerEm(self): 376 return self.fontInfo.get("unitsPerEm", 1000) 377 378 def getPSName(self): 379 return self.fontInfo.get("postscriptFontName", "PSName-Undefined") 380 381 @staticmethod 382 def isCID(): 383 return False 384 385 @staticmethod 386 def hasFDArray(): 387 return False 388 389 def convertToBez(self, name, read_hints, round_coords, doAll=False): 390 # We do not yet support reading hints, so read_hints is ignored. 391 width, bez, skip = self._get_or_skip_glyph(name, round_coords, doAll) 392 if skip: 393 return None 394 395 bezString = "\n".join(bez) 396 bezString = "\n".join(["% " + name, "sc", bezString, "ed", ""]) 397 return bezString 398 399 def updateFromBez(self, bezData, name, mm_hint_info=None): 400 layer = None 401 if name in self.processedLayerGlyphMap: 402 layer = PROCESSED_LAYER_NAME 403 glyphset = self._get_glyphset(layer) 404 405 glyph = BezGlyph(bezData) 406 glyphset.readGlyph(name, glyph) 407 if hasattr(glyph, 'width'): 408 glyph.width = norm_float(glyph.width) 409 self.newGlyphMap[name] = glyph 410 411 # updateFromBez is called only if the glyph has been autohinted which 412 # might also change its outline data. We need to update the edit status 413 # in the hash map entry. I assume that convertToBez has been run 414 # before, which will add an entry for this glyph. 415 self.updateHashEntry(name) 416 417 def save(self, path): 418 if path is None: 419 path = self.path 420 421 if os.path.abspath(self.path) != os.path.abspath(path): 422 # If user has specified a path other than the source font path, 423 # then copy the entire UFO font, and operate on the copy. 424 log.info("Copying from source UFO font to output UFO font before " 425 "processing...") 426 if os.path.exists(path): 427 shutil.rmtree(path) 428 shutil.copytree(self.path, path) 429 430 writer = UFOWriter(path, self._reader.formatVersion, validate=False) 431 432 layer = PROCESSED_LAYER_NAME 433 if self.writeToDefaultLayer: 434 layer = None 435 436 # Write layer contents. 437 layers = writer.layerContents.copy() 438 if self.writeToDefaultLayer and PROCESSED_LAYER_NAME in layers: 439 # Delete processed glyphs directory 440 writer.deleteGlyphSet(PROCESSED_LAYER_NAME) 441 # Remove entry from 'layercontents.plist' file 442 del layers[PROCESSED_LAYER_NAME] 443 elif self.processedLayerGlyphMap or not self.writeToDefaultLayer: 444 layers[PROCESSED_LAYER_NAME] = PROCESSED_GLYPHS_DIRNAME 445 writer.layerContents.update(layers) 446 writer.writeLayerContents() 447 448 # Write glyphs. 449 glyphset = writer.getGlyphSet(layer, defaultLayer=layer is None) 450 for name, glyph in self.newGlyphMap.items(): 451 filename = self.glyphMap[name] 452 if not self.writeToDefaultLayer and \ 453 name in self.processedLayerGlyphMap: 454 filename = self.processedLayerGlyphMap[name] 455 # Recalculate glyph hashes 456 if self.writeToDefaultLayer: 457 self.recalcHashEntry(name, glyph) 458 glyphset.contents[name] = filename 459 glyphset.writeGlyph(name, glyph, glyph.drawPoints) 460 glyphset.writeContents() 461 462 # Write hashmap 463 if self.hashMapChanged: 464 self.writeHashMap(writer) 465 466 @property 467 def hashMap(self): 468 if self._hashmap is None: 469 try: 470 data = self._reader.readData(HASHMAP_NAME) 471 except UFOLibError: 472 data = None 473 if data: 474 hashmap = ast.literal_eval(data.decode("utf-8")) 475 else: 476 hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION} 477 478 version = (0, 0) 479 if HASHMAP_VERSION_NAME in hashmap: 480 version = hashmap[HASHMAP_VERSION_NAME] 481 482 if version[0] > HASHMAP_VERSION[0]: 483 raise FontParseError("Hash map version is newer than " 484 "psautohint. Please update.") 485 elif version[0] < HASHMAP_VERSION[0]: 486 log.info("Updating hash map: was older version") 487 hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION} 488 489 self._hashmap = hashmap 490 return self._hashmap 491 492 def writeHashMap(self, writer): 493 hashMap = self.hashMap 494 if not hashMap: 495 return # no glyphs were processed. 496 497 data = ["{"] 498 for gName in sorted(hashMap.keys()): 499 data.append("'%s': %s," % (gName, hashMap[gName])) 500 data.append("}") 501 data.append("") 502 data = "\n".join(data) 503 504 writer.writeData(HASHMAP_NAME, data.encode("utf-8")) 505 506 def updateHashEntry(self, glyphName): 507 # srcHash has already been set: we are fixing the history list. 508 509 # Get hash entry for glyph 510 srcHash, historyList = self.hashMap[glyphName] 511 512 self.hashMapChanged = True 513 # If the program is not in the history list, add it. 514 if AUTOHINT_NAME not in historyList: 515 historyList.append(AUTOHINT_NAME) 516 517 def recalcHashEntry(self, glyphName, glyph): 518 hashBefore, historyList = self.hashMap[glyphName] 519 520 hash_pen = HashPointPen(glyph) 521 glyph.drawPoints(hash_pen) 522 hashAfter = hash_pen.getHash() 523 524 if hashAfter != hashBefore: 525 self.hashMap[glyphName] = [hashAfter, historyList] 526 self.hashMapChanged = True 527 528 def checkSkipGlyph(self, glyphName, newSrcHash, doAll): 529 skip = False 530 if self.log_only: 531 return skip 532 533 srcHash = None 534 historyList = [] 535 536 # Get hash entry for glyph 537 if glyphName in self.hashMap: 538 srcHash, historyList = self.hashMap[glyphName] 539 540 if srcHash == newSrcHash: 541 if AUTOHINT_NAME in historyList: 542 # The glyph has already been autohinted, and there have been no 543 # changes since. 544 skip = not doAll 545 if not skip and AUTOHINT_NAME not in historyList: 546 historyList.append(AUTOHINT_NAME) 547 else: 548 if CHECKOUTLINE_NAME in historyList: 549 log.error("Glyph '%s' has been edited. You must first " 550 "run '%s' before running '%s'. Skipping.", 551 glyphName, CHECKOUTLINE_NAME, AUTOHINT_NAME) 552 skip = True 553 554 # If the source hash has changed, we need to delete the processed 555 # layer glyph. 556 self.hashMapChanged = True 557 self.hashMap[glyphName] = [newSrcHash, [AUTOHINT_NAME]] 558 if glyphName in self.processedLayerGlyphMap: 559 del self.processedLayerGlyphMap[glyphName] 560 561 return skip 562 563 def _get_glyphset(self, layer_name=None): 564 if layer_name not in self._glyphsets: 565 glyphset = None 566 try: 567 glyphset = self._reader.getGlyphSet(layer_name) 568 except UFOLibError: 569 pass 570 self._glyphsets[layer_name] = glyphset 571 return self._glyphsets[layer_name] 572 573 @staticmethod 574 def get_glyph_bez(glyph, round_coords): 575 pen = BezPen(glyph.glyphSet, round_coords) 576 glyph.draw(pen) 577 if not hasattr(glyph, "width"): 578 glyph.width = 0 579 return pen.bez 580 581 def _get_or_skip_glyph(self, name, round_coords, doAll): 582 # Get default glyph layer data, so we can check if the glyph 583 # has been edited since this program was last run. 584 # If the program name is in the history list, and the srcHash 585 # matches the default glyph layer data, we can skip. 586 glyphset = self._get_glyphset() 587 glyph = glyphset[name] 588 bez = self.get_glyph_bez(glyph, round_coords) 589 590 # Hash is always from the default glyph layer. 591 hash_pen = HashPointPen(glyph) 592 glyph.drawPoints(hash_pen) 593 skip = self.checkSkipGlyph(name, hash_pen.getHash(), doAll) 594 595 # If there is a glyph in the processed layer, get the outline from it. 596 if name in self.processedLayerGlyphMap: 597 glyphset = self._get_glyphset(PROCESSED_LAYER_NAME) 598 glyph = glyphset[name] 599 bez = self.get_glyph_bez(glyph, round_coords) 600 601 return glyph.width, bez, skip 602 603 def getGlyphList(self): 604 glyphOrder = self._reader.readLib().get(PUBLIC_GLYPH_ORDER, []) 605 glyphList = list(self._get_glyphset().keys()) 606 607 # Sort the returned glyph list by the glyph order as we depend in the 608 # order for expanding glyph ranges. 609 def key_fn(v): 610 if v in glyphOrder: 611 return glyphOrder.index(v) 612 return len(glyphOrder) 613 return sorted(glyphList, key=key_fn) 614 615 @property 616 def glyphMap(self): 617 if self._glyphmap is None: 618 glyphset = self._get_glyphset() 619 self._glyphmap = glyphset.contents 620 return self._glyphmap 621 622 @property 623 def processedLayerGlyphMap(self): 624 if self._processed_layer_glyphmap is None: 625 self._processed_layer_glyphmap = {} 626 glyphset = self._get_glyphset(PROCESSED_LAYER_NAME) 627 if glyphset is not None: 628 self._processed_layer_glyphmap = glyphset.contents 629 return self._processed_layer_glyphmap 630 631 @property 632 def fontInfo(self): 633 if self._fontInfo is None: 634 info = SimpleNamespace() 635 self._reader.readInfo(info) 636 self._fontInfo = vars(info) 637 return self._fontInfo 638 639 def getFontInfo(self, allow_no_blues, noFlex, 640 vCounterGlyphs, hCounterGlyphs, fdIndex=0): 641 if self.fontDict is not None: 642 return self.fontDict 643 644 fdDict = fdTools.FDDict() 645 # should be 1 if the glyphs are ideographic, else 0. 646 fdDict.LanguageGroup = self.fontInfo.get("languagegroup", "0") 647 fdDict.OrigEmSqUnits = self.getUnitsPerEm() 648 fdDict.FontName = self.getPSName() 649 upm = self.getUnitsPerEm() 650 low = min(-upm * 0.25, 651 self.fontInfo.get("openTypeOS2WinDescent", 0) - 200) 652 high = max(upm * 1.25, 653 self.fontInfo.get("openTypeOS2WinAscent", 0) + 200) 654 # Make a set of inactive alignment zones: zones outside of the font 655 # bbox so as not to affect hinting. Used when src font has no 656 # BlueValues or has invalid BlueValues. Some fonts have bad BBox 657 # values, so I don't let this be smaller than -upm*0.25, upm*1.25. 658 inactiveAlignmentValues = [low, low, high, high] 659 blueValues = self.fontInfo.get("postscriptBlueValues", []) 660 numBlueValues = len(blueValues) 661 if numBlueValues < 4: 662 if allow_no_blues: 663 blueValues = inactiveAlignmentValues 664 numBlueValues = len(blueValues) 665 else: 666 raise FontParseError( 667 "Font must have at least four values in its " 668 "BlueValues array for PSAutoHint to work!") 669 blueValues.sort() 670 # The first pair only is a bottom zone, where the first value is the 671 # overshoot position; the rest are top zones, and second value of the 672 # pair is the overshoot position. 673 blueValues[0] = blueValues[0] - blueValues[1] 674 for i in range(3, numBlueValues, 2): 675 blueValues[i] = blueValues[i] - blueValues[i - 1] 676 677 blueValues = [str(v) for v in blueValues] 678 numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys)) 679 for i in range(numBlueValues): 680 key = fdTools.kBlueValueKeys[i] 681 value = blueValues[i] 682 setattr(fdDict, key, value) 683 684 otherBlues = self.fontInfo.get("postscriptOtherBlues", []) 685 686 if len(otherBlues) > 0: 687 i = 0 688 numBlueValues = len(otherBlues) 689 otherBlues.sort() 690 for i in range(0, numBlueValues, 2): 691 otherBlues[i] = otherBlues[i] - otherBlues[i + 1] 692 otherBlues = [str(v) for v in otherBlues] 693 numBlueValues = min(numBlueValues, 694 len(fdTools.kOtherBlueValueKeys)) 695 for i in range(numBlueValues): 696 key = fdTools.kOtherBlueValueKeys[i] 697 value = otherBlues[i] 698 setattr(fdDict, key, value) 699 700 vstems = self.fontInfo.get("postscriptStemSnapV", []) 701 if not vstems: 702 if allow_no_blues: 703 # dummy value. Needs to be larger than any hint will likely be, 704 # as the autohint program strips out any hint wider than twice 705 # the largest global stem width. 706 vstems = [fdDict.OrigEmSqUnits] 707 else: 708 raise FontParseError("Font does not have postscriptStemSnapV!") 709 710 vstems.sort() 711 if not vstems or (len(vstems) == 1 and vstems[0] < 1): 712 # dummy value that will allow PyAC to run 713 vstems = [fdDict.OrigEmSqUnits] 714 log.warning("There is no value or 0 value for DominantV.") 715 fdDict.DominantV = "[" + " ".join([str(v) for v in vstems]) + "]" 716 717 hstems = self.fontInfo.get("postscriptStemSnapH", []) 718 if not hstems: 719 if allow_no_blues: 720 # dummy value. Needs to be larger than any hint will likely be, 721 # as the autohint program strips out any hint wider than twice 722 # the largest global stem width. 723 hstems = [fdDict.OrigEmSqUnits] 724 else: 725 raise FontParseError("Font does not have postscriptStemSnapH!") 726 727 hstems.sort() 728 if not hstems or (len(hstems) == 1 and hstems[0] < 1): 729 # dummy value that will allow PyAC to run 730 hstems = [fdDict.OrigEmSqUnits] 731 log.warning("There is no value or 0 value for DominantH.") 732 fdDict.DominantH = "[" + " ".join([str(v) for v in hstems]) + "]" 733 734 if noFlex: 735 fdDict.FlexOK = "false" 736 else: 737 fdDict.FlexOK = "true" 738 739 # Add candidate lists for counter hints, if any. 740 if vCounterGlyphs: 741 temp = " ".join(vCounterGlyphs) 742 fdDict.VCounterChars = "( %s )" % (temp) 743 if hCounterGlyphs: 744 temp = " ".join(hCounterGlyphs) 745 fdDict.HCounterChars = "( %s )" % (temp) 746 747 fdDict.BlueFuzz = self.fontInfo.get("postscriptBlueFuzz", 1) 748 # postscriptBlueShift 749 # postscriptBlueScale 750 self.fontDict = fdDict 751 return fdDict 752 753 def getfdInfo(self, allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, 754 glyphList, fdIndex=0): 755 fdGlyphDict = None 756 fdDict = self.getFontInfo(allow_no_blues, noFlex, 757 vCounterGlyphs, hCounterGlyphs, fdIndex) 758 fontDictList = [fdDict] 759 760 # Check the fontinfo file, and add any other font dicts 761 srcFontInfo = os.path.dirname(self.path) 762 srcFontInfo = os.path.join(srcFontInfo, "fontinfo") 763 maxX = self.getUnitsPerEm() * 2 764 maxY = maxX 765 minY = -self.getUnitsPerEm() 766 if os.path.exists(srcFontInfo): 767 with open(srcFontInfo, "r", encoding="utf-8") as fi: 768 fontInfoData = fi.read() 769 fontInfoData = re.sub(r"#[^\r\n]+", "", fontInfoData) 770 771 if "FDDict" in fontInfoData: 772 fdGlyphDict, fontDictList, finalFDict = \ 773 fdTools.parseFontInfoFile( 774 fontDictList, fontInfoData, glyphList, maxY, minY, 775 self.getPSName()) 776 if finalFDict is None: 777 # If a font dict was not explicitly specified for the 778 # output font, use the first user-specified font dict. 779 fdTools.mergeFDDicts(fontDictList[1:], self.fontDict) 780 else: 781 fdTools.mergeFDDicts([finalFDict], self.fontDict) 782 783 return fdGlyphDict, fontDictList 784 785 @staticmethod 786 def close(): 787 return 788 789 790class BezPen(BasePen): 791 def __init__(self, glyph_set, round_coords): 792 super(BezPen, self).__init__(glyph_set) 793 self.round_coords = round_coords 794 self.bez = [] 795 796 def _point(self, point): 797 if self.round_coords: 798 return " ".join("%d" % round(pt) for pt in point) 799 return " ".join("%3f" % pt for pt in point) 800 801 def _moveTo(self, pt): 802 self.bez.append("%s mt" % self._point(pt)) 803 804 def _lineTo(self, pt): 805 self.bez.append("%s dt" % self._point(pt)) 806 807 def _curveToOne(self, pt1, pt2, pt3): 808 self.bez.append("%s ct" % self._point(pt1 + pt2 + pt3)) 809 810 @staticmethod 811 def _qCurveToOne(pt1, pt2): 812 raise FontParseError("Quadratic curves are not supported") 813 814 def _closePath(self): 815 self.bez.append("cp") 816 817 818class HashPointPen(AbstractPointPen): 819 820 def __init__(self, glyph): 821 self.glyphset = getattr(glyph, "glyphSet", None) 822 self.width = norm_float(round(getattr(glyph, "width", 0), 9)) 823 self.data = ["w%s" % self.width] 824 825 def getHash(self): 826 data = "".join(self.data) 827 if len(data) >= 128: 828 data = hashlib.sha512(data.encode("ascii")).hexdigest() 829 return data 830 831 def beginPath(self, identifier=None, **kwargs): 832 pass 833 834 def endPath(self): 835 pass 836 837 def addPoint(self, pt, segmentType=None, smooth=False, name=None, 838 identifier=None, **kwargs): 839 if segmentType is None: 840 pt_type = "" 841 else: 842 pt_type = segmentType[0] 843 self.data.append("%s%s%s" % (pt_type, 844 repr(norm_float(round(pt[0], 9))), 845 repr(norm_float(round(pt[1], 9))))) 846 847 def addComponent(self, baseGlyphName, transformation, identifier=None, 848 **kwargs): 849 self.data.append("base:%s" % baseGlyphName) 850 851 for v in transformation: 852 self.data.append(str(norm_float(round(v, 9)))) 853 854 self.data.append("w%s" % self.width) 855 glyph = self.glyphset[baseGlyphName] 856 glyph.drawPoints(self) 857 858 859class BezGlyph(object): 860 def __init__(self, bez): 861 self._bez = bez 862 self.lib = {} 863 864 @staticmethod 865 def _draw(contours, pen): 866 for contour in contours: 867 pen.beginPath() 868 for point in contour: 869 x = point.get("x") 870 y = point.get("y") 871 segmentType = point.get("type", None) 872 name = point.get("name", None) 873 pen.addPoint((x, y), segmentType=segmentType, name=name) 874 pen.endPath() 875 876 def drawPoints(self, pen): 877 contours, hints = convertBezToOutline(self._bez) 878 self._draw(contours, pen) 879 880 # Add the stem hints. 881 if hints is not None: 882 # Add this hash to the glyph data, as it is the hash which matches 883 # the output outline data. This is not necessarily the same as the 884 # hash of the source data; autohint can be used to change outlines. 885 hash_pen = HashPointPen(self) 886 self._draw(contours, hash_pen) 887 hints["id"] = hash_pen.getHash() 888 889 # Remove any existing hint data. 890 for key in (HINT_DOMAIN_NAME1, HINT_DOMAIN_NAME2): 891 if key in self.lib: 892 del self.lib[key] 893 894 self.lib[HINT_DOMAIN_NAME2] = hints 895 896 897class HintMask: 898 # class used to collect hints for the current 899 # hint mask when converting bez to T2. 900 def __init__(self, listPos): 901 # The index into the pointList is kept 902 # so we can quickly find them later. 903 self.listPos = listPos 904 self.hList = [] # These contain the actual hint values. 905 self.vList = [] 906 self.hstem3List = [] 907 self.vstem3List = [] 908 # The name attribute of the point which follows the new hint set. 909 self.pointName = "hintSet" + str(listPos).zfill(4) 910 911 def getHintSet(self): 912 hintset = OrderedDict() 913 hintset[POINT_TAG] = self.pointName 914 hintset[STEMS_NAME] = [] 915 916 if len(self.hList) > 0 or len(self.hstem3List): 917 hintset[STEMS_NAME].extend( 918 makeHintSet(self.hList, self.hstem3List, isH=True)) 919 920 if len(self.vList) > 0 or len(self.vstem3List): 921 hintset[STEMS_NAME].extend( 922 makeHintSet(self.vList, self.vstem3List, isH=False)) 923 924 return hintset 925 926 927def norm_float(value): 928 """Converts a float (whose decimal part is zero) to integer""" 929 if isinstance(value, float) and value.is_integer(): 930 return int(value) 931 return value 932 933 934def makeStemHintList(hintsStem3, isH): 935 # In bez terms, the first coordinate in each pair is 936 # absolute, second is relative, and hence is the width. 937 if isH: 938 op = HSTEM3_NAME 939 else: 940 op = VSTEM3_NAME 941 posList = [op] 942 for stem3 in hintsStem3: 943 for pos, width in stem3: 944 posList.append("%s %s" % (norm_float(pos), norm_float(width))) 945 return " ".join(posList) 946 947 948def makeHintList(hints, isH): 949 # Add the list of hint operators 950 # In bez terms, the first coordinate in each pair is 951 # absolute, second is relative, and hence is the width. 952 hintset = [] 953 for hint in hints: 954 if not hint: 955 continue 956 pos = hint[0] 957 width = hint[1] 958 if isH: 959 op = HSTEM_NAME 960 else: 961 op = VSTEM_NAME 962 hintset.append("%s %s %s" % (op, norm_float(pos), norm_float(width))) 963 return hintset 964 965 966def fixStartPoint(contour, operators): 967 # For the GLIF format, the idea of first/last point is funky, because 968 # the format avoids identifying a start point. This means there is no 969 # implied close-path line-to. If the last implied or explicit path-close 970 # operator is a line-to, then replace the "mt" with linto, and remove 971 # the last explicit path-closing line-to, if any. If the last op is a 972 # curve, then leave the first two point args on the stack at the end of 973 # the point list, and move the last curveto to the first op, replacing 974 # the move-to. 975 976 _, firstX, firstY = operators[0] 977 lastOp, lastX, lastY = operators[-1] 978 point = contour[0] 979 if (firstX == lastX) and (firstY == lastY): 980 del contour[-1] 981 point["type"] = lastOp 982 else: 983 # we have an implied final line to. All we need to do 984 # is convert the inital moveto to a lineto. 985 point["type"] = "line" 986 987 988bezToUFOPoint = { 989 "mt": 'move', 990 "rmt": 'move', 991 "dt": 'line', 992 "ct": 'curve', 993} 994 995 996def convertCoords(current_x, current_y): 997 return norm_float(current_x), norm_float(current_y) 998 999 1000def convertBezToOutline(bezString): 1001 """ 1002 Since the UFO outline element as no attributes to preserve, 1003 I can just make a new one. 1004 """ 1005 # convert bez data to a UFO glif XML representation 1006 # 1007 # Convert all bez ops to simplest UFO equivalent 1008 # Add all hints to vertical and horizontal hint lists as encountered; 1009 # insert a HintMask class whenever a new set of hints is encountered 1010 # after all operators have been processed, convert HintMask items into 1011 # hintmask ops and hintmask bytes add all hints as prefix review operator 1012 # list to optimize T2 operators. 1013 # if useStem3 == 1, then any counter hints must be processed as stem3 1014 # hints, else the opposite. 1015 # Counter hints are used only in LanguageGroup 1 glyphs, aka ideographs 1016 1017 bezString = re.sub(r"%.+?\n", "", bezString) # supress comments 1018 bez = re.findall(r"(\S+)", bezString) 1019 flexes = [] 1020 # Create an initial hint mask. We use this if 1021 # there is no explicit initial hint sub. 1022 hintmask = HintMask(0) 1023 hintmasks = [hintmask] 1024 vstem3_args = [] 1025 hstem3_args = [] 1026 args = [] 1027 operators = [] 1028 hintmask_name = None 1029 in_preflex = False 1030 hints = None 1031 op_index = 0 1032 current_x = 0 1033 current_y = 0 1034 contours = [] 1035 contour = None 1036 has_hints = False 1037 1038 for token in bez: 1039 try: 1040 val = float(token) 1041 args.append(val) 1042 continue 1043 except ValueError: 1044 pass 1045 if token == "newcolors": 1046 pass 1047 elif token in ["beginsubr", "endsubr"]: 1048 pass 1049 elif token == "snc": 1050 hintmask = HintMask(op_index) 1051 # If the new hints precedes any marking operator, 1052 # then we want throw away the initial hint mask we 1053 # made, and use the new one as the first hint mask. 1054 if op_index == 0: 1055 hintmasks = [hintmask] 1056 else: 1057 hintmasks.append(hintmask) 1058 hintmask_name = hintmask.pointName 1059 elif token == "enc": 1060 pass 1061 elif token == "rb": 1062 if hintmask_name is None: 1063 hintmask_name = hintmask.pointName 1064 hintmask.hList.append(args) 1065 args = [] 1066 has_hints = True 1067 elif token == "ry": 1068 if hintmask_name is None: 1069 hintmask_name = hintmask.pointName 1070 hintmask.vList.append(args) 1071 args = [] 1072 has_hints = True 1073 elif token == "rm": # vstem3's are vhints 1074 if hintmask_name is None: 1075 hintmask_name = hintmask.pointName 1076 has_hints = True 1077 vstem3_args.append(args) 1078 args = [] 1079 if len(vstem3_args) == 3: 1080 hintmask.vstem3List.append(vstem3_args) 1081 vstem3_args = [] 1082 1083 elif token == "rv": # hstem3's are hhints 1084 has_hints = True 1085 hstem3_args.append(args) 1086 args = [] 1087 if len(hstem3_args) == 3: 1088 hintmask.hstem3List.append(hstem3_args) 1089 hstem3_args = [] 1090 1091 elif token == "preflx1": 1092 # the preflx1/preflx2a sequence provides the same i as the flex 1093 # sequence; the difference is that the preflx1/preflx2a sequence 1094 # provides the argument values needed for building a Type1 string 1095 # while the flex sequence is simply the 6 rcurveto points. Both 1096 # sequences are always provided. 1097 args = [] 1098 # need to skip all move-tos until we see the "flex" operator. 1099 in_preflex = True 1100 elif token == "preflx2a": 1101 args = [] 1102 elif token == "flxa": # flex with absolute coords. 1103 in_preflex = False 1104 flex_point_name = BASE_FLEX_NAME + str(op_index).zfill(4) 1105 flexes.append(flex_point_name) 1106 # The first 12 args are the 6 args for each of 1107 # the two curves that make up the flex feature. 1108 i = 0 1109 while i < 2: 1110 current_x = args[0] 1111 current_y = args[1] 1112 x, y = convertCoords(current_x, current_y) 1113 point = {"x": x, "y": y} 1114 contour.append(point) 1115 current_x = args[2] 1116 current_y = args[3] 1117 x, y = convertCoords(current_x, current_y) 1118 point = {"x": x, "y": y} 1119 contour.append(point) 1120 current_x = args[4] 1121 current_y = args[5] 1122 x, y = convertCoords(current_x, current_y) 1123 point_type = 'curve' 1124 point = {"x": x, "y": y, "type": point_type} 1125 contour.append(point) 1126 operators.append([point_type, current_x, current_y]) 1127 op_index += 1 1128 if i == 0: 1129 args = args[6:12] 1130 i += 1 1131 # attach the point name to the first point of the first curve. 1132 contour[-6][POINT_NAME] = flex_point_name 1133 if hintmask_name is not None: 1134 # We have a hint mask that we want to attach to the first 1135 # point of the flex op. However, there is already a flex 1136 # name in that attribute. What we do is set the flex point 1137 # name into the hint mask. 1138 hintmask.pointName = flex_point_name 1139 hintmask_name = None 1140 args = [] 1141 elif token == "sc": 1142 pass 1143 elif token == "cp": 1144 pass 1145 elif token == "ed": 1146 pass 1147 else: 1148 if in_preflex and token in ["rmt", "mt"]: 1149 continue 1150 1151 if token in ["rmt", "mt", "dt", "ct"]: 1152 op_index += 1 1153 else: 1154 raise BezParseError( 1155 "Unhandled operation: '%s' '%s'." % (args, token)) 1156 point_type = bezToUFOPoint[token] 1157 if token in ["rmt", "mt", "dt"]: 1158 if token in ["mt", "dt"]: 1159 current_x = args[0] 1160 current_y = args[1] 1161 else: 1162 current_x += args[0] 1163 current_y += args[1] 1164 x, y = convertCoords(current_x, current_y) 1165 point = {"x": x, "y": y, "type": point_type} 1166 1167 if point_type == "move": 1168 if contour is not None: 1169 if len(contour) == 1: 1170 # Just in case we see two moves in a row, 1171 # delete the previous contour if it has 1172 # only the move-to 1173 log.info("Deleting moveto: %s adding %s", 1174 contours[-1], contour) 1175 del contours[-1] 1176 else: 1177 # Fix the start/implied end path 1178 # of the previous path. 1179 fixStartPoint(contour, operators) 1180 operators = [] 1181 contour = [] 1182 contours.append(contour) 1183 1184 if hintmask_name is not None: 1185 point[POINT_NAME] = hintmask_name 1186 hintmask_name = None 1187 contour.append(point) 1188 operators.append([point_type, current_x, current_y]) 1189 else: # "ct" 1190 current_x = args[0] 1191 current_y = args[1] 1192 x, y = convertCoords(current_x, current_y) 1193 point = {"x": x, "y": y} 1194 contour.append(point) 1195 current_x = args[2] 1196 current_y = args[3] 1197 x, y = convertCoords(current_x, current_y) 1198 point = {"x": x, "y": y} 1199 contour.append(point) 1200 current_x = args[4] 1201 current_y = args[5] 1202 x, y = convertCoords(current_x, current_y) 1203 point = {"x": x, "y": y, "type": point_type} 1204 contour.append(point) 1205 if hintmask_name is not None: 1206 # attach the pointName to the first point of the curve. 1207 contour[-3][POINT_NAME] = hintmask_name 1208 hintmask_name = None 1209 operators.append([point_type, current_x, current_y]) 1210 args = [] 1211 1212 if contour is not None: 1213 if len(contour) == 1: 1214 # Just in case we see two moves in a row, delete 1215 # the previous contour if it has zero length. 1216 del contours[-1] 1217 else: 1218 fixStartPoint(contour, operators) 1219 1220 # Add hints, if any. 1221 # Must be done at the end of op processing to make sure we have seen all 1222 # the hints in the bez string. 1223 # Note that the hintmasks are identified in the operators by the point 1224 # name. We will follow the T1 spec: a glyph may have stem3 counter hints 1225 # or regular hints, but not both. 1226 1227 if has_hints or len(flexes) > 0: 1228 hints = OrderedDict() 1229 hints["id"] = "" 1230 1231 # Convert the rest of the hint masks to a hintmask op and hintmask 1232 # bytes. 1233 hints[HINT_SET_LIST_NAME] = [] 1234 for hintmask in hintmasks: 1235 hints[HINT_SET_LIST_NAME].append(hintmask.getHintSet()) 1236 1237 if len(flexes) > 0: 1238 hints[FLEX_INDEX_LIST_NAME] = [] 1239 for pointTag in flexes: 1240 hints[FLEX_INDEX_LIST_NAME].append(pointTag) 1241 1242 return contours, hints 1243 1244 1245def makeHintSet(hints, hintsStem3, isH): 1246 # A charstring may have regular v stem hints or vstem3 hints, but not both. 1247 # Same for h stem hints and hstem3 hints. 1248 hintset = [] 1249 if len(hintsStem3) > 0: 1250 hintsStem3.sort() 1251 numHints = len(hintsStem3) 1252 hintLimit = int((STACK_LIMIT - 2) / 2) 1253 if numHints >= hintLimit: 1254 hintsStem3 = hintsStem3[:hintLimit] 1255 hintset.append(makeStemHintList(hintsStem3, isH)) 1256 else: 1257 hints.sort() 1258 numHints = len(hints) 1259 hintLimit = int((STACK_LIMIT - 2) / 2) 1260 if numHints >= hintLimit: 1261 hints = hints[:hintLimit] 1262 hintset.extend(makeHintList(hints, isH)) 1263 1264 return hintset 1265