Ejemplo n.º 1
0
def main(argv=None):
  argparser = ArgumentParser(
    description='Remove glyphs from all UFOs in src dir')

  argparser.add_argument(
    '-dry', dest='dryRun', action='store_const', const=True, default=False,
    help='Do not modify anything, but instead just print what would happen.')

  argparser.add_argument(
    '-decompose', dest='decompose', action='store_const', const=True, default=False,
    help='When deleting a glyph which is used as a component by another glyph '+
         'which is not being deleted, instead of refusing to delete the glyph, '+
         'decompose the component instances in other glyphs.')

  argparser.add_argument(
    '-ignore-git-state', dest='ignoreGitState', action='store_const', const=True, default=False,
    help='Skip checking with git if there are changes to the target UFO file.')

  argparser.add_argument(
    'glyphs', metavar='<glyph>', type=str, nargs='+',
    help='Glyph to remove. '+
         'Can be a glyphname, '+
         'a Unicode code point formatted as "U+<CP>", '+
         'or a Unicode code point range formatted as "U+<CP>-<CP>"')

  args = argparser.parse_args(argv)
  global dryRun
  dryRun = args.dryRun
  srcDir = os.path.join(BASEDIR, 'src')

  # check if src font has modifications
  if not args.ignoreGitState:
    gitStatus = subprocess.check_output(
      ['git', '-C', BASEDIR, 'status', '-s', '--',
       os.path.relpath(os.path.abspath(srcDir), BASEDIR) ],
      shell=False)
    gitIsDirty = False
    gitStatusLines = gitStatus.splitlines()
    for line in gitStatusLines:
      if len(line) > 3 and line[:2] != '??':
        gitIsDirty = True
        break
    if gitIsDirty:
      if len(gitStatusLines) > 5:
        gitStatusLines = gitStatusLines[:5] + [' ...']
      print(
        ("%s has uncommitted changes. It's strongly recommended to run this "+
         "script on an unmodified UFO path so to allow \"undoing\" any changes. "+
         "Run with -ignore-git-state to ignore this warning.\n%s") % (
         srcDir, '\n'.join(gitStatusLines)),
        file=sys.stderr)
      sys.exit(1)

  # Find UFO fonts
  fontPaths = glob.glob(os.path.join(srcDir, '*.ufo'))
  if len(fontPaths) == 0:
    print('No UFOs found in', srcDir, file=sys.stderr)
    sys.exit(1)

  # load fontbuild config
  config = RawConfigParser(dict_type=OrderedDict)
  configFilename = os.path.join(srcDir, 'fontbuild.cfg')
  config.read(configFilename)
  glyphOrderFile = configFindResFile(config, srcDir, 'glyphorder')
  diacriticsFile = configFindResFile(config, srcDir, 'diacriticfile')
  featuresFile = os.path.join(srcDir, 'features.fea')

  # load AGL and diacritics
  agl = loadAGL(os.path.join(srcDir, 'glyphlist.txt')) # { 2126: 'Omega', ... }
  comps = loadGlyphCompositions(diacriticsFile)
    # { glyphName => (baseName, accentNames, offset) }

  # find glyphnames to remove that are composed (removal happens later)
  rmnamesUnion = getGlyphNamesComps(comps, agl, args.glyphs)

  # find glyphnames to remove from UFOs (and remove them) 
  for fontPath in fontPaths:
    relFontPath = os.path.relpath(fontPath, BASEDIR)
    print('Loading glyph data for %s...' % relFontPath)
    font = OpenFont(fontPath)
    ucmap = font.getCharacterMapping()  # { 2126: [ 'Omega', ...], ...}
    cnmap = font.getReverseComponentMapping()  # { 'A' : ['Aacute', 'Aring'], 'acute' : ['Aacute'] ... }

    glyphnames = getGlyphNamesFont(font, ucmap, args.glyphs)

    if len(glyphnames) == 0:
      print('None of the glyphs requested exist in', relFontPath, file=sys.stderr)

    print('Preparing to remove %d glyphs — resolving component usage...' % len(glyphnames))

    # Check component usage
    cnConflicts = {}
    for gname in glyphnames:
      cnUses = cnmap.get(gname)
      if cnUses:
        extCnUses = [n for n in cnUses if n not in glyphnames]
        if len(extCnUses) > 0:
          cnConflicts[gname] = extCnUses

    if len(cnConflicts) > 0:
      if args.decompose:
        componentsToDecompose = set()
        for gname in cnConflicts.keys():
          componentsToDecompose.add(gname)
        for gname, dependants in cnConflicts.iteritems():
          print('decomposing %s in %s' % (gname, ', '.join(dependants)))
          for depname in dependants:
            decomposeComponentInstances(font, font[depname], componentsToDecompose)
      else:
        print(
          '\nComponent conflicts.\n\n'+
          'Some glyphs to-be deleted are used as components in other glyphs.\n'+
          'You need to either decompose the components, also delete glyphs\n'+
          'using them, or not delete the glyphs at all.\n', file=sys.stderr)
        for gname, dependants in cnConflicts.iteritems():
          print('%s used by %s' % (gname, ', '.join(dependants)), file=sys.stderr)
        sys.exit(1)

    # find orphaned pure-components
    for gname in glyphnames:
      try:
        g = font[gname]
      except:
        print('no glyph %r in %s' % (gname, relFontPath), file=sys.stderr)
        sys.exit(1)
      useCount = 0
      for cn in g.components:
        usedBy = cnmap.get(cn.baseGlyph)
        if usedBy:
          usedBy = [name for name in usedBy if name not in glyphnames]
          if len(usedBy) == 0:
            cng = font[cn.baseGlyph]
            if len(cng.unicodes) == 0:
              print('Note: pure-component %s orphaned' % cn.baseGlyph)

    # remove glyphs from UFO
    print('Removing %d glyphs' % len(glyphnames))

    libPlistFilename = os.path.join(fontPath, 'lib.plist')
    libPlist = plistlib.readPlist(libPlistFilename)
    
    glyphOrder = libPlist.get('public.glyphOrder')
    if glyphOrder is not None:
      v = [name for name in glyphOrder if name not in glyphnames]
      libPlist['public.glyphOrder'] = v

    roboSort = libPlist.get('com.typemytype.robofont.sort')
    if roboSort is not None:
      for entry in roboSort:
        if isinstance(entry, dict) and entry.get('type') == 'glyphList':
          asc = entry.get('ascending')
          if asc is not None:
            entry['ascending'] = [name for name in asc if name not in glyphnames]
          desc = entry.get('descending')
          if desc is not None:
            entry['descending'] = [name for name in desc if name not in glyphnames]

    for gname in glyphnames:
      font.removeGlyph(gname)
      rmnamesUnion.add(gname)

    if not dryRun:
      print('Writing changes to %s' % relFontPath)
      font.save()
      plistlib.writePlist(libPlist, libPlistFilename)
    else:
      print('Writing changes to %s (dry run)' % relFontPath)

    print('Cleaning up kerning')
    if dryRun:
      cleanup_kerning.main(['-dry', fontPath])
    else:
      cleanup_kerning.main([fontPath])

  # end for fontPath in fontPaths


  # fontbuild config
  updateDiacriticsFile(diacriticsFile, rmnamesUnion)
  updateConfigFile(config, configFilename, rmnamesUnion)
  featuresChanged = updateFeaturesFile(featuresFile, rmnamesUnion)


  # TMP for testing fuzzy
  # rmnamesUnion = set()
  # featuresChanged = False
  # with open('_local/rmlog') as f:
  #   for line in f:
  #     line = line.strip()
  #     if len(line):
  #       rmnamesUnion.add(line)


  print('\n————————————————————————————————————————————————————\n'+
        'Removed %d glyphs:\n  %s' % (
          len(rmnamesUnion), '\n  '.join(sorted(rmnamesUnion))))

  print('\n————————————————————————————————————————————————————\n')

  # find possibly-missed instances
  print('Fuzzy matches:')
  fuzzyMatchCount = 0
  fuzzyMatchCount += grep(diacriticsFile, rmnamesUnion)
  fuzzyMatchCount += grep(configFilename, rmnamesUnion)
  fuzzyMatchCount += grep(featuresFile, rmnamesUnion)
  for fontPath in fontPaths:
    fuzzyMatchCount += grep(os.path.join(fontPath, 'lib.plist'), rmnamesUnion)
  if fuzzyMatchCount == 0:
    print('  (none)\n')
  else:
    print('You may want to look into those ^\n')

  if featuresChanged:
    print('You need to manually edit features.\n'+
          '- git diff src/features.fea\n'+
          '- $EDITOR %s/features.fea\n' % '/features.fea\n- $EDITOR '.join(fontPaths))

  print(('You need to re-generate %s via\n'+
         '`make src/glyphorder.txt` (or misc/gen-glyphorder.py)'
        ) % glyphOrderFile)

  print('\nFinally, you should build the Medium weight and make sure it all '+
        'looks good and that no mixglyph failures occur. E.g. `make Medium -j`')
Ejemplo n.º 2
0
def main():
    argparser = argparse.ArgumentParser(description='Enrich UFO glyphnames')

    argparser.add_argument(
        '-dry',
        dest='dryRun',
        action='store_const',
        const=True,
        default=False,
        help='Do not modify anything, but instead just print what would happen.'
    )

    argparser.add_argument(
        '-list-missing',
        dest='listMissing',
        action='store_const',
        const=True,
        default=False,
        help=
        'List glyphs with unicodes found in source files but missing in any of the target UFOs.'
    )

    argparser.add_argument(
        '-list-unnamed',
        dest='listUnnamed',
        action='store_const',
        const=True,
        default=False,
        help=
        "List glyphs with unicodes in target UFOs that don't have symbolic names."
    )

    argparser.add_argument(
        '-backfill-agl',
        dest='backfillWithAgl',
        action='store_const',
        const=True,
        default=False,
        help=
        "Use glyphnames from Adobe Glyph List for any glyphs that no names in any of"
        + " the input font files")

    argparser.add_argument(
        '-src',
        dest='srcFonts',
        metavar='<fontfile>',
        type=str,
        nargs='*',
        help='TrueType, OpenType or UFO fonts to gather glyph info from. ' +
        'Names found in earlier-listed fonts are prioritized over later listings.'
    )

    argparser.add_argument('dstFonts',
                           metavar='<ufofile>',
                           type=str,
                           nargs='+',
                           help='UFO fonts to update')

    args = argparser.parse_args()

    # Load UFO fonts
    dstFonts = []
    dstFontPaths = {}  # keyed by RFont object
    srcDir = None
    for fn in args.dstFonts:
        fn = fn.rstrip('/')
        font = OpenFont(fn)
        dstFonts.append(font)
        dstFontPaths[font] = fn
        srcDir2 = os.path.dirname(fn)
        if srcDir is None:
            srcDir = srcDir2
        elif srcDir != srcDir2:
            raise Exception('All <ufofile>s must be rooted in same directory')

    # load fontbuild configuration
    config = RawConfigParser(dict_type=OrderedDict)
    configFilename = os.path.join(srcDir, 'fontbuild.cfg')
    config.read(configFilename)
    glyphOrderFile = configFindResFile(config, srcDir, 'glyphorder')
    diacriticsFile = configFindResFile(config, srcDir, 'diacriticfile')
    glyphOrder = readGlyphOrderFile(glyphOrderFile)

    fallbackGlyphNames = {}  # { 2126: 'Omega', ... }
    if args.backfillWithAgl:
        fallbackGlyphNames = parseAGL(
            configFindResFile(config, srcDir, 'agl_glyphlistfile'))

    # find glyph names
    uc2names, extraUc2names, name2ucsv = buildGlyphNames(
        dstFonts, args.srcFonts, glyphOrder, fallbackGlyphNames)
    # Note: name2ucsv has same order as parameters to buildGlyphNames

    if args.listMissing:
        print('# Missing glyphs: (found in -src but not in any <ufofile>)')
        for uc, names in extraUc2names.iteritems():
            print('U+%04X\t%s' % (uc, ', '.join(names)))
        return

    elif args.listUnnamed:
        print('# Unnamed glyphs:')
        unnamed = set()
        for name in glyphOrder:
            if len(name) > 7 and name.startswith('uni'):
                unnamed.add(name)
        for gl in name2ucsv[:len(dstFonts)]:
            for name, ucs in gl.iteritems():
                for uc in ucs:
                    if isDefaultGlyphNameForUnicode(name, uc):
                        unnamed.add(name)
                        break
        for name in unnamed:
            print(name)
        return

    printDry = lambda *args: print(*args)
    if args.dryRun:
        printDry = lambda *args: print('[dry-run]', *args)

    newNames = {}
    renameGlyphsQueue = {}  # keyed by RFont object

    for font in dstFonts:
        renameGlyphsQueue[font] = {}

    for uc, names in uc2names.iteritems():
        if len(names) < 2:
            continue
        dstGlyphName = names[0]
        if isDefaultGlyphNameForUnicode(dstGlyphName, uc):
            newGlyphName = getFirstNonDefaultGlyphName(uc, names[1:])
            # if newGlyphName is None:
            #   # if we found no symbolic name, check in fallback list
            #   newGlyphName = fallbackGlyphNames.get(uc)
            #   if newGlyphName is not None:
            #     printDry('Using fallback %s' % newGlyphName)
            if newGlyphName is not None:
                printDry('Rename %s -> %s' % (dstGlyphName, newGlyphName))
                for font in dstFonts:
                    if dstGlyphName in font:
                        renameGlyphsQueue[font][dstGlyphName] = newGlyphName
                newNames[dstGlyphName] = newGlyphName

    if len(newNames) == 0:
        printDry('No changes')
        return

    # rename component instances
    for font in dstFonts:
        componentMap = font.getReverseComponentMapping()
        for currName, newName in renameGlyphsQueue[font].iteritems():
            for depName in componentMap.get(currName, []):
                depG = font[depName]
                for c in depG.components:
                    if c.baseGlyph == currName:
                        c.baseGlyph = newName
                        c.setChanged()

    # rename glyphs
    for font in dstFonts:
        for currName, newName in renameGlyphsQueue[font].iteritems():
            font[currName].name = newName

    # save fonts and update font data
    for font in dstFonts:
        fontPath = dstFontPaths[font]
        printDry('Saving %d glyphs in %s' % (len(newNames), fontPath))
        if not args.dryRun:
            font.save()
        renameUFODetails(font,
                         fontPath,
                         newNames,
                         dryRun=args.dryRun,
                         print=printDry)

    # update resource files
    renameGlyphOrderFile(glyphOrderFile,
                         newNames,
                         dryRun=args.dryRun,
                         print=printDry)
    renameDiacriticsFile(diacriticsFile,
                         newNames,
                         dryRun=args.dryRun,
                         print=printDry)
    renameConfigFile(config,
                     configFilename,
                     newNames,
                     dryRun=args.dryRun,
                     print=printDry)