1# Copyright 2016 Adobe. All rights reserved. 2 3# Methods: 4 5# Parse args. If glyphlist is from file, read in entire file as single string, 6# and remove all white space, then parse out glyph-names and GID's. 7 8# For each font name: 9# Use fontTools library to open font and extract CFF table. 10# If error, skip font and report error. 11# Filter specified glyph list, if any, with list of glyphs in the font. 12# Open font plist file, if any. If not, create empty font plist. 13# Build alignment zone string 14# For identifier in glyph-list: 15# Get T2 charstring for glyph from parent font CFF table. If not present, 16# report and skip. 17# Get new alignment zone string if FDarray index (which font dict is used) 18# has changed. 19# Convert to bez 20# Build autohint point list string; this is used to tell if glyph has been 21# changed since the last time it was hinted. 22# If requested, check against plist dict, and skip if glyph is already 23# hinted or is manually hinted. 24# Call autohint library on bez string. 25# If change to the point list is permitted and happened, rebuild. 26# Autohint point list string. 27# Convert bez string to T2 charstring, and update parent font CFF. 28# Add glyph hint entry to plist file 29# Save font plist file. 30 31import ast 32import logging 33import os 34import re 35import time 36from collections import defaultdict, namedtuple 37 38from .otfFont import CFFFontData 39from .ufoFont import UFOFontData 40from ._psautohint import error as PsAutoHintCError 41 42from . import (get_font_format, hint_bez_glyph, hint_compatible_bez_glyphs, 43 FontParseError) 44 45log = logging.getLogger(__name__) 46 47 48class ACOptions(object): 49 def __init__(self): 50 self.inputPaths = [] 51 self.outputPaths = [] 52 self.reference_font = None 53 self.glyphList = [] 54 self.nameAliases = {} 55 self.excludeGlyphList = False 56 self.hintAll = False 57 self.read_hints = False 58 self.allowChanges = False 59 self.noFlex = False 60 self.noHintSub = False 61 self.allow_no_blues = False 62 self.hCounterGlyphs = [] 63 self.vCounterGlyphs = [] 64 self.logOnly = False 65 self.printDefaultFDDict = False 66 self.printFDDictList = False 67 self.round_coords = True 68 self.writeToDefaultLayer = False 69 self.baseMaster = {} 70 self.font_format = None 71 self.report_zones = False 72 self.report_stems = False 73 self.report_all_stems = False 74 75 def __str__(self): 76 # used only when debugging. 77 import inspect 78 data = [] 79 methodList = inspect.getmembers(self) 80 for fname, fvalue in methodList: 81 if fname[0] == "_": 82 continue 83 data.append(str((fname, fvalue))) 84 data.append("") 85 return os.linesep.join(data) 86 87 88class ACHintError(Exception): 89 pass 90 91 92class GlyphReports: 93 def __init__(self): 94 self.glyphs = {} 95 96 def addGlyphReport(self, glyphName, reportString): 97 hstems = {} 98 vstems = {} 99 hstems_pos = {} 100 vstems_pos = {} 101 char_zones = {} 102 stem_zone_stems = {} 103 self.glyphs[glyphName] = [hstems, vstems, char_zones, stem_zone_stems] 104 105 lines = reportString.splitlines() 106 for line in lines: 107 tokens = line.split() 108 key = tokens[0] 109 x = ast.literal_eval(tokens[3]) 110 y = ast.literal_eval(tokens[5]) 111 hintpos = "%s %s" % (x, y) 112 if key == "charZone": 113 char_zones[hintpos] = (x, y) 114 elif key == "stemZone": 115 stem_zone_stems[hintpos] = (x, y) 116 elif key == "HStem": 117 width = x - y 118 # avoid counting duplicates 119 if hintpos not in hstems_pos: 120 count = hstems.get(width, 0) 121 hstems[width] = count + 1 122 hstems_pos[hintpos] = width 123 elif key == "VStem": 124 width = x - y 125 # avoid counting duplicates 126 if hintpos not in vstems_pos: 127 count = vstems.get(width, 0) 128 vstems[width] = count + 1 129 vstems_pos[hintpos] = width 130 else: 131 raise FontParseError("Found unknown keyword %s in report file " 132 "for glyph %s." % (key, glyphName)) 133 134 @staticmethod 135 def round_value(val): 136 if val >= 0: 137 return int(val + 0.5) 138 else: 139 return int(val - 0.5) 140 141 def parse_stem_dict(self, stem_dict): 142 """ 143 stem_dict: {45.5: 1, 47.0: 2} 144 """ 145 # key: stem width 146 # value: stem count 147 width_dict = defaultdict(int) 148 for width, count in stem_dict.items(): 149 width = self.round_value(width) 150 width_dict[width] += count 151 return width_dict 152 153 def parse_zone_dicts(self, char_dict, stem_dict): 154 all_zones_dict = char_dict.copy() 155 all_zones_dict.update(stem_dict) 156 # key: zone height 157 # value: zone count 158 top_dict = defaultdict(int) 159 bot_dict = defaultdict(int) 160 for top, bot in all_zones_dict.values(): 161 top = self.round_value(top) 162 top_dict[top] += 1 163 bot = self.round_value(bot) 164 bot_dict[bot] += 1 165 return top_dict, bot_dict 166 167 def assemble_rep_list(self, items_dict, count_dict): 168 # item 0: stem/zone count 169 # item 1: stem width/zone height 170 # item 2: list of glyph names 171 gorder = list(self.glyphs.keys()) 172 rep_list = [] 173 for item in items_dict: 174 gnames = list(items_dict[item]) 175 # sort the names by the font's glyph order 176 if len(gnames) > 1: 177 gindexes = [gorder.index(gname) for gname in gnames] 178 gnames = [x for _, x in sorted(zip(gindexes, gnames))] 179 rep_list.append((count_dict[item], item, gnames)) 180 return rep_list 181 182 def _get_lists(self): 183 """ 184 self.glyphs is a dictionary: 185 key: glyph name 186 value: list of 4 dictionaries 187 hstems 188 vstems 189 char_zones 190 stem_zone_stems 191 { 192 'A': [{45.5: 1, 47.0: 2}, {229.0: 1}, {}, {}], 193 'B': [{46.0: 2, 46.5: 2, 47.0: 1}, {94.0: 1, 100.0: 1}, {}, {}], 194 'C': [{50.0: 2}, {109.0: 1}, {}, {}], 195 'D': [{46.0: 1, 46.5: 2, 47.0: 1}, {95.0: 1, 109.0: 1}, {}, {}], 196 'E': [{46.5: 2, 47.0: 1, 50.0: 2, 177.0: 1, 178.0: 1}, 197 {46.0: 1, 75.5: 2, 95.0: 1}, {}, {}], 198 'F': [{46.5: 2, 47.0: 1, 50.0: 1, 177.0: 1}, 199 {46.0: 1, 60.0: 1, 75.5: 1, 95.0: 1}, {}, {}], 200 'G': [{43.0: 1, 44.5: 1, 50.0: 1}, {94.0: 1, 109.0: 1}, {}, {}] 201 } 202 """ 203 h_stem_items_dict = defaultdict(set) 204 h_stem_count_dict = defaultdict(int) 205 v_stem_items_dict = defaultdict(set) 206 v_stem_count_dict = defaultdict(int) 207 208 top_zone_items_dict = defaultdict(set) 209 top_zone_count_dict = defaultdict(int) 210 bot_zone_items_dict = defaultdict(set) 211 bot_zone_count_dict = defaultdict(int) 212 213 for gName, dicts in self.glyphs.items(): 214 hStemDict, vStemDict, charZoneDict, stemZoneStemDict = dicts 215 216 glyph_h_stem_dict = self.parse_stem_dict(hStemDict) 217 glyph_v_stem_dict = self.parse_stem_dict(vStemDict) 218 219 for stem_width, stem_count in glyph_h_stem_dict.items(): 220 h_stem_items_dict[stem_width].add(gName) 221 h_stem_count_dict[stem_width] += stem_count 222 223 for stem_width, stem_count in glyph_v_stem_dict.items(): 224 v_stem_items_dict[stem_width].add(gName) 225 v_stem_count_dict[stem_width] += stem_count 226 227 glyph_top_zone_dict, glyph_bot_zone_dict = self.parse_zone_dicts( 228 charZoneDict, stemZoneStemDict) 229 230 for zone_height, zone_count in glyph_top_zone_dict.items(): 231 top_zone_items_dict[zone_height].add(gName) 232 top_zone_count_dict[zone_height] += zone_count 233 234 for zone_height, zone_count in glyph_bot_zone_dict.items(): 235 bot_zone_items_dict[zone_height].add(gName) 236 bot_zone_count_dict[zone_height] += zone_count 237 238 # item 0: stem count 239 # item 1: stem width 240 # item 2: list of glyph names 241 h_stem_list = self.assemble_rep_list( 242 h_stem_items_dict, h_stem_count_dict) 243 244 v_stem_list = self.assemble_rep_list( 245 v_stem_items_dict, v_stem_count_dict) 246 247 # item 0: zone count 248 # item 1: zone height 249 # item 2: list of glyph names 250 top_zone_list = self.assemble_rep_list( 251 top_zone_items_dict, top_zone_count_dict) 252 253 bot_zone_list = self.assemble_rep_list( 254 bot_zone_items_dict, bot_zone_count_dict) 255 256 return h_stem_list, v_stem_list, top_zone_list, bot_zone_list 257 258 @staticmethod 259 def _sort_count(t): 260 """ 261 sort by: count (1st item), value (2nd item), list of glyph names (3rd 262 item) 263 """ 264 return (-t[0], -t[1], t[2]) 265 266 @staticmethod 267 def _sort_val(t): 268 """ 269 sort by: value (2nd item), count (1st item), list of glyph names (3rd 270 item) 271 """ 272 return (t[1], -t[0], t[2]) 273 274 @staticmethod 275 def _sort_val_reversed(t): 276 """ 277 sort by: value (2nd item), count (1st item), list of glyph names (3rd 278 item) 279 """ 280 return (-t[1], -t[0], t[2]) 281 282 def save(self, path): 283 h_stems, v_stems, top_zones, bot_zones = self._get_lists() 284 items = ([h_stems, self._sort_count], 285 [v_stems, self._sort_count], 286 [top_zones, self._sort_val_reversed], 287 [bot_zones, self._sort_val]) 288 atime = time.asctime() 289 suffixes = (".hstm.txt", ".vstm.txt", ".top.txt", ".bot.txt") 290 titles = ("Horizontal Stem List for %s on %s\n" % (path, atime), 291 "Vertical Stem List for %s on %s\n" % (path, atime), 292 "Top Zone List for %s on %s\n" % (path, atime), 293 "Bottom Zone List for %s on %s\n" % (path, atime), 294 ) 295 headers = (["count width glyphs\n"] * 2 + 296 ["count height glyphs\n"] * 2) 297 298 for i, item in enumerate(items): 299 reps, sortFunc = item 300 if not reps: 301 continue 302 fName = f'{path}{suffixes[i]}' 303 title = titles[i] 304 header = headers[i] 305 with open(fName, "w") as fp: 306 fp.write(title) 307 fp.write(header) 308 reps.sort(key=sortFunc) 309 for rep in reps: 310 gnames = ' '.join(rep[2]) 311 fp.write(f"{rep[0]:5} {rep[1]:5} [{gnames}]\n") 312 log.info("Wrote %s" % fName) 313 314 315def getGlyphID(glyphTag, fontGlyphList): 316 if glyphTag in fontGlyphList: 317 return fontGlyphList.index(glyphTag) 318 319 return None 320 321 322def getGlyphNames(glyphTag, fontGlyphList, fontFileName): 323 glyphNameList = [] 324 rangeList = glyphTag.split("-") 325 prevGID = getGlyphID(rangeList[0], fontGlyphList) 326 if prevGID is None: 327 if len(rangeList) > 1: 328 log.warning("glyph ID <%s> in range %s from glyph selection " 329 "list option is not in font. <%s>.", 330 rangeList[0], glyphTag, fontFileName) 331 else: 332 log.warning("glyph ID <%s> from glyph selection list option " 333 "is not in font. <%s>.", rangeList[0], fontFileName) 334 return None 335 glyphNameList.append(fontGlyphList[prevGID]) 336 337 for glyphTag2 in rangeList[1:]: 338 gid = getGlyphID(glyphTag2, fontGlyphList) 339 if gid is None: 340 log.warning("glyph ID <%s> in range %s from glyph selection " 341 "list option is not in font. <%s>.", 342 glyphTag2, glyphTag, fontFileName) 343 return None 344 for i in range(prevGID + 1, gid + 1): 345 glyphNameList.append(fontGlyphList[i]) 346 prevGID = gid 347 348 return glyphNameList 349 350 351def filterGlyphList(options, fontGlyphList, fontFileName): 352 # Return the list of glyphs which are in the intersection of the argument 353 # list and the glyphs in the font. 354 # Complain about glyphs in the argument list which are not in the font. 355 if not options.glyphList: 356 glyphList = fontGlyphList 357 else: 358 # expand ranges: 359 glyphList = [] 360 for glyphTag in options.glyphList: 361 glyphNames = getGlyphNames(glyphTag, fontGlyphList, fontFileName) 362 if glyphNames is not None: 363 glyphList.extend(glyphNames) 364 if options.excludeGlyphList: 365 newList = filter(lambda name: name not in glyphList, fontGlyphList) 366 glyphList = newList 367 return glyphList 368 369 370fontInfoKeywordList = [ 371 'FontName', # string 372 'OrigEmSqUnits', 373 'LanguageGroup', 374 'DominantV', # array 375 'DominantH', # array 376 'FlexOK', # string 377 'BlueFuzz', 378 'VCounterChars', # counter 379 'HCounterChars', # counter 380 'BaselineYCoord', 381 'BaselineOvershoot', 382 'CapHeight', 383 'CapOvershoot', 384 'LcHeight', 385 'LcOvershoot', 386 'AscenderHeight', 387 'AscenderOvershoot', 388 'FigHeight', 389 'FigOvershoot', 390 'Height5', 391 'Height5Overshoot', 392 'Height6', 393 'Height6Overshoot', 394 'DescenderOvershoot', 395 'DescenderHeight', 396 'SuperiorOvershoot', 397 'SuperiorBaseline', 398 'OrdinalOvershoot', 399 'OrdinalBaseline', 400 'Baseline5Overshoot', 401 'Baseline5', 402 'Baseline6Overshoot', 403 'Baseline6', 404] 405 406integerPattern = r""" -?\d+""" 407arrayPattern = r""" \[[ ,0-9]+\]""" 408stringPattern = r""" \S+""" 409counterPattern = r""" \([\S ]+\)""" 410 411 412def printFontInfo(fontInfoString): 413 for item in fontInfoKeywordList: 414 if item in ['FontName', 'FlexOK']: 415 matchingExp = item + stringPattern 416 elif item in ['VCounterChars', 'HCounterChars']: 417 matchingExp = item + counterPattern 418 elif item in ['DominantV', 'DominantH']: 419 matchingExp = item + arrayPattern 420 else: 421 matchingExp = item + integerPattern 422 423 try: 424 print('\t%s' % re.search(matchingExp, fontInfoString).group()) 425 except Exception: 426 pass 427 428 429def openFile(path, options): 430 font_format = get_font_format(path) 431 if font_format is None: 432 raise FontParseError(f"{path} is not a supported font format") 433 434 if font_format == "UFO": 435 font = UFOFontData(path, options.logOnly, options.writeToDefaultLayer) 436 else: 437 font = CFFFontData(path, font_format) 438 439 return font 440 441 442def get_glyph_list(options, font, path): 443 filename = os.path.basename(path) 444 445 # filter specified list, if any, with font list. 446 glyph_list = filterGlyphList(options, font.getGlyphList(), filename) 447 if not glyph_list: 448 raise FontParseError("Selected glyph list is empty for font <%s>." % 449 filename) 450 451 return glyph_list 452 453 454def get_bez_glyphs(options, font, glyph_list): 455 glyphs = {} 456 457 for name in glyph_list: 458 # Convert to bez format 459 try: 460 bez_glyph = font.convertToBez(name, options.read_hints, 461 options.round_coords, 462 options.hintAll) 463 464 if bez_glyph is None or "mt" not in bez_glyph: 465 # skip empty glyphs. 466 continue 467 except KeyError: 468 # Source fonts may be sparse, e.g. be a subset of the 469 # reference font. 470 bez_glyph = None 471 glyphs[name] = GlyphEntry(bez_glyph, font) 472 473 total = len(glyph_list) 474 processed = len(glyphs) 475 if processed != total: 476 log.info("Skipped %s of %s glyphs.", total - processed, total) 477 478 return glyphs 479 480 481def get_fontinfo_list(options, font, path, glyph_list, is_var): 482 483 # Check counter glyphs, if any. 484 counter_glyphs = options.hCounterGlyphs + options.vCounterGlyphs 485 if counter_glyphs: 486 missing = [n for n in counter_glyphs if n not in font.getGlyphList()] 487 if missing: 488 log.error("H/VCounterChars glyph named in fontinfo is " 489 "not in font: %s", missing) 490 491 # For Type1 name keyed fonts, psautohint supports defining 492 # different alignment zones for different glyphs by FontDict 493 # entries in the fontinfo file. This is NOT supported for CID 494 # or CFF2 fonts, as these have FDArrays, can can truly support 495 # different Font.Dict.Private Dicts for different groups of glyphs. 496 if font.hasFDArray(): 497 return get_fontinfo_list_withFDArray(options, font, path, 498 glyph_list, is_var) 499 else: 500 return get_fontinfo_list_withFontInfo(options, font, path, glyph_list) 501 502 503def get_fontinfo_list_withFDArray(options, font, path, glyph_list, is_var): 504 lastFDIndex = None 505 fontinfo_list = {} 506 for name in glyph_list: 507 # get new fontinfo string if FDarray index has changed, 508 # as each FontDict has different alignment zones. 509 fdIndex = font.getfdIndex(name) 510 if not fdIndex == lastFDIndex: 511 lastFDIndex = fdIndex 512 fddict = font.getFontInfo(options.allow_no_blues, 513 options.noFlex, 514 options.vCounterGlyphs, 515 options.hCounterGlyphs, 516 fdIndex) 517 fontinfo = fddict.getFontInfo() 518 fontinfo_list[name] = (fontinfo, None, None) 519 520 return fontinfo_list 521 522 523def get_fontinfo_list_withFontInfo(options, font, path, glyph_list): 524 # Build alignment zone string 525 if options.printDefaultFDDict: 526 print("Showing default FDDict Values:") 527 fddict = font.getFontInfo(options.allow_no_blues, 528 options.noFlex, 529 options.vCounterGlyphs, 530 options.hCounterGlyphs) 531 printFontInfo(str(fddict)) 532 return 533 534 fdglyphdict, fontDictList = font.getfdInfo(options.allow_no_blues, 535 options.noFlex, 536 options.vCounterGlyphs, 537 options.hCounterGlyphs, 538 glyph_list) 539 540 if options.printFDDictList: 541 # Print the user defined FontDicts, and exit. 542 if fdglyphdict: 543 print("Showing user-defined FontDict Values:\n") 544 for fi, fontDict in enumerate(fontDictList): 545 print(fontDict.DictName) 546 printFontInfo(str(fontDict)) 547 gnameList = [] 548 # item = [glyphName, [fdIndex, glyphListIndex]] 549 itemList = sorted(fdglyphdict.items(), key=lambda x: x[1][1]) 550 for gName, entry in itemList: 551 if entry[0] == fi: 552 gnameList.append(gName) 553 print("%d glyphs:" % len(gnameList)) 554 if len(gnameList) > 0: 555 gTxt = " ".join(gnameList) 556 else: 557 gTxt = "None" 558 print(gTxt + "\n") 559 else: 560 print("There are no user-defined FontDict Values.") 561 return 562 563 if fdglyphdict is None: 564 fddict = fontDictList[0] 565 fontinfo = fddict.getFontInfo() 566 else: 567 log.info("Using alternate FDDict global values from fontinfo " 568 "file for some glyphs.") 569 570 lastFDIndex = None 571 fontinfo_list = {} 572 for name in glyph_list: 573 if fdglyphdict is not None: 574 fdIndex = fdglyphdict[name][0] 575 if lastFDIndex != fdIndex: 576 lastFDIndex = fdIndex 577 fddict = fontDictList[fdIndex] 578 fontinfo = fddict.getFontInfo() 579 580 fontinfo_list[name] = (fontinfo, fddict, fdglyphdict) 581 582 return fontinfo_list 583 584 585class MMHintInfo: 586 def __init__(self, glyph_name=None): 587 self.defined = False 588 self.h_order = None 589 self.v_order = None 590 self.hint_masks = [] 591 self.glyph_name = glyph_name 592 # bad_hint_idxs contains the hint pair indices for all the bad 593 # hint pairs in any of the fonts for the current glyph. 594 self.bad_hint_idxs = set() 595 self.cntr_masks = [] 596 self.new_cntr_masks = [] 597 self.glyph_programs = None 598 599 @property 600 def needs_fix(self): 601 return len(self.bad_hint_idxs) > 0 602 603 604def hint_glyph(options, name, bez_glyph, fontinfo): 605 try: 606 hinted = hint_bez_glyph(fontinfo, bez_glyph, options.allowChanges, 607 not options.noHintSub, options.round_coords, 608 options.report_zones, options.report_stems, 609 options.report_all_stems) 610 except PsAutoHintCError: 611 raise ACHintError("%s: Failure in processing outline data." % 612 options.nameAliases.get(name, name)) 613 614 return hinted 615 616 617def hint_compatible_glyphs(options, name, bez_glyphs, masters, fontinfo): 618 # This function is used by both 619 # hint_with_reference_font->hint_compatible_fonts 620 # and hint_vf_font. 621 try: 622 ref_master = masters[0] 623 # ************************************************************* 624 # *********** DO NOT DELETE THIS COMMENTED-OUT CODE *********** 625 # If you're tempted to "clean up", work on solving 626 # https://github.com/adobe-type-tools/psautohint/issues/202 627 # first, then you can uncomment the "hint_compatible_bez_glyphs" 628 # line and remove this and other related comments, as well as 629 # the workaround block following "# else:", below. Thanks. 630 # ************************************************************* 631 # 632 # if False: 633 # # This is disabled because it causes crashes on the CI servers 634 # # which are not reproducible locally. The branch below is a hack 635 # # to avoid the crash and should be dropped once the crash is 636 # # fixed, https://github.com/adobe-type-tools/psautohint/pull/131 637 # hinted = hint_compatible_bez_glyphs( 638 # fontinfo, bez_glyphs, masters) 639 # *** see https://github.com/adobe-type-tools/psautohint/issues/202 *** 640 # else: 641 hinted = [] 642 hinted_ref_bez = hint_glyph(options, name, bez_glyphs[0], fontinfo) 643 for i, bez in enumerate(bez_glyphs[1:]): 644 if bez is None: 645 out = [hinted_ref_bez, None] 646 else: 647 in_bez = [hinted_ref_bez, bez] 648 in_masters = [ref_master, masters[i + 1]] 649 out = hint_compatible_bez_glyphs(fontinfo, 650 in_bez, 651 in_masters) 652 # out is [hinted_ref_bez, new_hinted_region_bez] 653 if i == 0: 654 hinted = out 655 else: 656 hinted.append(out[1]) 657 except PsAutoHintCError: 658 raise ACHintError("%s: Failure in processing outline data." % 659 options.nameAliases.get(name, name)) 660 661 return hinted 662 663 664def get_glyph_reports(options, font, glyph_list, fontinfo_list): 665 reports = GlyphReports() 666 667 glyphs = get_bez_glyphs(options, font, glyph_list) 668 for name in glyphs: 669 if name == ".notdef": 670 continue 671 672 bez_glyph = glyphs[name][0] 673 fontinfo = fontinfo_list[name][0] 674 675 report = hint_glyph(options, name, bez_glyph, fontinfo) 676 reports.addGlyphReport(name, report.strip()) 677 678 return reports 679 680 681GlyphEntry = namedtuple("GlyphEntry", "bez_data,font") 682 683 684def hint_font(options, font, glyph_list, fontinfo_list): 685 aliases = options.nameAliases 686 687 hinted = {} 688 glyphs = get_bez_glyphs(options, font, glyph_list) 689 for name in glyphs: 690 g_entry = glyphs[name] 691 fontinfo, fddict, fdglyphdict = fontinfo_list[name] 692 693 if fdglyphdict: 694 log.info("%s: Begin hinting (using fdDict %s).", 695 aliases.get(name, name), fddict.DictName) 696 else: 697 log.info("%s: Begin hinting.", aliases.get(name, name)) 698 699 # Call auto-hint library on bez string. 700 new_bez_glyph = hint_glyph(options, name, g_entry.bez_data, fontinfo) 701 options.baseMaster[name] = new_bez_glyph 702 703 if not ("ry" in new_bez_glyph or "rb" in new_bez_glyph or 704 "rm" in new_bez_glyph or "rv" in new_bez_glyph): 705 log.info("%s: No hints added!", aliases.get(name, name)) 706 continue 707 708 if options.logOnly: 709 continue 710 711 hinted[name] = GlyphEntry(new_bez_glyph, font) 712 713 return hinted 714 715 716def hint_compatible_fonts(options, paths, glyphs, 717 fontinfo_list): 718 # glyphs is a list of dicts, one per font. Each dict is keyed by glyph name 719 # and references a tuple of (src bez file, font) 720 aliases = options.nameAliases 721 722 hinted_glyphs = set() 723 reference_font = None 724 725 for name in glyphs[0]: 726 fontinfo, _, _ = fontinfo_list[name] 727 728 log.info("%s: Begin hinting.", aliases.get(name, name)) 729 730 masters = [os.path.basename(path) for path in paths] 731 bez_glyphs = [g[name].bez_data for g in glyphs] 732 new_bez_glyphs = hint_compatible_glyphs(options, name, bez_glyphs, 733 masters, fontinfo) 734 if options.logOnly: 735 continue 736 737 if reference_font is None: 738 fonts = [g[name].font for g in glyphs] 739 reference_font = fonts[0] 740 mm_hint_info = MMHintInfo() 741 742 for i, new_bez_glyph in enumerate(new_bez_glyphs): 743 if new_bez_glyph is not None: 744 g_entry = glyphs[i][name] 745 g_entry.font.updateFromBez(new_bez_glyph, name, mm_hint_info) 746 747 hinted_glyphs.add(name) 748 # Now check if we need to fix any hint lists. 749 if mm_hint_info.needs_fix: 750 reference_font.fix_glyph_hints(name, mm_hint_info, 751 is_reference_font=True) 752 for font in fonts[1:]: 753 font.fix_glyph_hints(name, 754 mm_hint_info, 755 is_reference_font=False) 756 757 return len(hinted_glyphs) > 0 758 759 760def hint_vf_font(options, font_path, out_path): 761 font = openFile(font_path, options) 762 options.noFlex = True # work around for incompatibel flex args. 763 aliases = options.nameAliases 764 glyph_names = get_glyph_list(options, font, font_path) 765 log.info("Hinting font %s. Start time: %s.", font_path, time.asctime()) 766 fontinfo_list = get_fontinfo_list(options, font, font_path, 767 glyph_names, True) 768 hinted_glyphs = set() 769 770 for name in glyph_names: 771 fontinfo, _, _ = fontinfo_list[name] 772 log.info("%s: Begin hinting.", aliases.get(name, name)) 773 774 bez_glyphs = font.get_vf_bez_glyphs(name) 775 num_masters = len(bez_glyphs) 776 masters = [f"Master-{i}" for i in range(num_masters)] 777 new_bez_glyphs = hint_compatible_glyphs(options, name, bez_glyphs, 778 masters, fontinfo) 779 if None in new_bez_glyphs: 780 log.info(f"Error while hinting glyph {name}.") 781 continue 782 if options.logOnly: 783 continue 784 hinted_glyphs.add(name) 785 786 # First, convert bez to fontTools T2 programs, 787 # and check if any hints conflict. 788 mm_hint_info = MMHintInfo() 789 for i, new_bez_glyph in enumerate(new_bez_glyphs): 790 if new_bez_glyph is not None: 791 font.updateFromBez(new_bez_glyph, name, mm_hint_info) 792 793 # Now check if we need to fix any hint lists. 794 if mm_hint_info.needs_fix: 795 font.fix_glyph_hints(name, mm_hint_info) 796 797 # Now merge the programs into a singel CFF2 charstring program 798 font.merge_hinted_glyphs(name) 799 800 if hinted_glyphs: 801 log.info(f"Saving font file {out_path} with new hints...") 802 font.save(out_path) 803 else: 804 log.info("No glyphs were hinted.") 805 font.close() 806 807 log.info("Done with font %s. End time: %s.", font_path, time.asctime()) 808 809 810def hint_with_reference_font(options, fonts, paths, outpaths): 811 # We are doing compatible, AKA multiple master, hinting. 812 log.info("Start time: %s.", time.asctime()) 813 options.noFlex = True # work-around for mm-hinting 814 815 # Get the glyphs and font info of the reference font. We assume the 816 # fonts have the same glyph set, glyph dict and in general are 817 # compatible. If not bad things will happen. 818 glyph_names = get_glyph_list(options, fonts[0], paths[0]) 819 fontinfo_list = get_fontinfo_list(options, fonts[0], paths[0], 820 glyph_names, False) 821 822 glyphs = [] 823 for i, font in enumerate(fonts): 824 glyphs.append(get_bez_glyphs(options, font, glyph_names)) 825 826 have_hinted_glyphs = hint_compatible_fonts(options, paths, 827 glyphs, fontinfo_list) 828 if have_hinted_glyphs: 829 log.info("Saving font files with new hints...") 830 831 for i, font in enumerate(fonts): 832 font.save(outpaths[i]) 833 else: 834 log.info("No glyphs were hinted.") 835 font.close() 836 837 log.info("End time: %s.", time.asctime()) 838 839 840def hint_regular_fonts(options, fonts, paths, outpaths): 841 # Regular fonts, just iterate over the list and hint each one. 842 for i, font in enumerate(fonts): 843 path = paths[i] 844 outpath = outpaths[i] 845 846 glyph_names = get_glyph_list(options, font, path) 847 fontinfo_list = get_fontinfo_list(options, font, path, glyph_names, 848 False) 849 850 log.info("Hinting font %s. Start time: %s.", path, time.asctime()) 851 852 if options.report_zones or options.report_stems: 853 reports = get_glyph_reports(options, font, glyph_names, 854 fontinfo_list) 855 reports.save(outpath) 856 else: 857 hinted = hint_font(options, font, glyph_names, fontinfo_list) 858 if hinted: 859 log.info("Saving font file with new hints...") 860 for name in hinted: 861 g_entry = hinted[name] 862 font.updateFromBez(g_entry.bez_data, name) 863 font.save(outpath) 864 else: 865 log.info("No glyphs were hinted.") 866 font.close() 867 868 log.info("Done with font %s. End time: %s.", path, time.asctime()) 869 870 871def get_outpath(options, font_path, i): 872 if options.outputPaths is not None and i < len(options.outputPaths): 873 outpath = options.outputPaths[i] 874 else: 875 outpath = font_path 876 return outpath 877 878 879def hintFiles(options): 880 fonts = [] 881 paths = [] 882 outpaths = [] 883 # If there is a reference font, prepend it to font paths. 884 # It must be the first font in the list, code below assumes that. 885 if options.reference_font: 886 font = openFile(options.reference_font, options) 887 fonts.append(font) 888 paths.append(options.reference_font) 889 outpaths.append(options.reference_font) 890 if hasattr(font, 'ttFont'): 891 assert 'fvar' not in font.ttFont, ("Can't use a CFF2 VF font as a " 892 "default font in a set of MM " 893 "fonts.") 894 895 # Open the rest of the fonts and handle output paths. 896 for i, path in enumerate(options.inputPaths): 897 font = openFile(path, options) 898 out_path = get_outpath(options, path, i) 899 if hasattr(font, 'ttFont') and 'fvar' in font.ttFont: 900 assert not options.report_zones or options.report_stems 901 # Certainly not supported now, also I think it only makes sense 902 # to ask for zone reports for the source fonts for the VF font. 903 # You can't easily change blue values in a VF font. 904 hint_vf_font(options, path, out_path) 905 else: 906 fonts.append(font) 907 paths.append(path) 908 outpaths.append(out_path) 909 910 if fonts: 911 if fonts[0].isCID(): 912 options.noFlex = True # Flex hinting in CJK fonts doed bad things. 913 # For CFF fonts, being a CID font is a good indicator of being CJK. 914 915 if options.reference_font: 916 hint_with_reference_font(options, fonts, paths, outpaths) 917 else: 918 hint_regular_fonts(options, fonts, paths, outpaths) 919