def loadLocalNamesDB(agl, diacriticComps):  # { 2126: ['Omega', ...], ...}
    uc2names = None

    for fontPath in srcFontPaths:
        font = OpenFont(fontPath)
        if uc2names is None:
            uc2names = font.getCharacterMapping(
            )  # { 2126: ['Omega', ...], ...}
        else:
            for uc, names in font.getCharacterMapping().iteritems():
                names2 = uc2names.get(uc, [])
                for name in names:
                    if name not in names2:
                        names2.append(name)
                uc2names[uc] = names2

    # agl { 2126: 'Omega', ...} -> { 'Omega': [2126, ...], ...}
    aglName2Ucs = {}
    for uc, name in agl.iteritems():
        aglName2Ucs.setdefault(name, []).append(uc)

    for glyphName, comp in diacriticComps.iteritems():
        for uc in aglName2Ucs.get(glyphName, []):
            names = uc2names.get(uc, [])
            if glyphName not in names:
                names.append(glyphName)
            uc2names[uc] = names

    name2ucs = {}
    for uc, names in uc2names.iteritems():
        for name in names:
            name2ucs.setdefault(name, set()).add(uc)

    return uc2names, name2ucs
Exemplo n.º 2
0
def main():
  argparser = ArgumentParser(
    description='Generate kerning samples by providing the left-hand side glyph')

  argparser.add_argument(
    '-u', dest='formatAsUnicode', action='store_const', const=True, default=False,
    help='Format output as unicode escape sequences instead of glyphnames. ' +
         'E.g. "\\u2126" instead of "\\Omega"')

  argparser.add_argument(
    '-suffix', dest='suffix', metavar='<text>', type=str,
    help='Text to append after each pair')

  argparser.add_argument(
    '-all-in-groups', dest='includeAllInGroup',
    action='store_const', const=True, default=False,
    help='Include all glyphs for groups rather than just the first glyph listed.')

  argparser.add_argument(
    'fontPath', metavar='<ufofile>', type=str, help='UFO font source')

  argparser.add_argument(
    'glyphnames', metavar='<glyphname>', type=str, nargs='+',
    help='Name of glyphs to generate samples for. '+
         'You can also provide a Unicode code point using the syntax "U+XXXX"')

  args = argparser.parse_args()

  font = OpenFont(args.fontPath)

  groupsFilename = os.path.join(args.fontPath, 'groups.plist')
  kerningFilename = os.path.join(args.fontPath, 'kerning.plist')

  groups = plistlib.readPlist(groupsFilename)   # { groupName => [glyphName] }
  kerning = plistlib.readPlist(kerningFilename) # { leftName => {rightName => kernVal} }
  groupmap = mapGroups(groups)

  # expand any unicode codepoints
  glyphnames = []
  for glyphname in args.glyphnames:
    if len(glyphname) > 2 and glyphname[:2] == 'U+':
      cp = int(glyphname[2:], 16)
      ucmap = font.getCharacterMapping()  # { 2126: ['Omega', ...], ...}
      for glyphname2 in ucmap[cp]:
        glyphnames.append(glyphname2)
    else:
      glyphnames.append(glyphname)

  for glyphname in glyphnames:
    samplesForGlyphname(font, groups, groupmap, kerning, glyphname, args)
Exemplo n.º 3
0
def loadUFOGlyphNames(ufoPath):
    font = OpenFont(ufoPath)

    libPlist = PList(os.path.join(ufoPath, 'lib.plist'))
    orderedNames = libPlist['public.glyphOrder']  # [ 'Omega', ...]

    # append any glyphs that are missing in orderedNames
    allNames = set(font.keys())
    for name in orderedNames:
        allNames.discard(name)
    for name in allNames:
        orderedNames.append(name)

    ucToNames = font.getCharacterMapping()  # { 2126: [ 'Omega', ...], ...}
    nameToUc = revCharMap(ucToNames)  # { 'Omega': 2126, ...}

    gol = OrderedDict()  # OrderedDict{ ('Omega', 2126|None), ...}
    for name in orderedNames:
        gol[name] = nameToUc.get(name)
        # gol.append((name, nameToUc.get(name)))

    return gol, ucToNames, nameToUc, libPlist
Exemplo n.º 4
0
def main():
    jsonSchemaDescr = '{[unicode:int]: glyphname:string, ...}'

    argparser = ArgumentParser(
        description=
        'Rename glyphnames in UFO kerning and remove unused groups and 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('-no-stats',
                           dest='noStats',
                           action='store_const',
                           const=True,
                           default=False,
                           help='Do not print statistics at the end.')

    argparser.add_argument('-save-stats',
                           dest='saveStatsPath',
                           metavar='<file>',
                           type=str,
                           help='Write detailed statistics to JSON file.')

    argparser.add_argument('-src-json',
                           dest='srcJSONFile',
                           metavar='<file>',
                           type=str,
                           help='JSON file to read glyph names from.' +
                           ' Expected schema: ' + jsonSchemaDescr +
                           ' (e.g. {2126: "Omega"})')

    argparser.add_argument(
        '-src-font',
        dest='srcFontFile',
        metavar='<file>',
        type=str,
        help='TrueType or OpenType font to read glyph names from.')

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

    args = argparser.parse_args()
    dryRun = args.dryRun

    if args.srcJSONFile and args.srcFontFile:
        argparser.error(
            'Both -src-json and -src-font specified -- please provide only one.'
        )

    # Strip trailing slashes from font paths
    args.dstFontsPaths = [s.rstrip('/ ') for s in args.dstFontsPaths]

    # Load source char map
    srcCharMap = None
    if args.srcJSONFile:
        try:
            srcCharMap = loadJSONCharMap(args.srcJSONFile)
        except Exception as err:
            argparser.error('Invalid JSON: Expected schema %s (%s)' %
                            (jsonSchemaDescr, err))
    elif args.srcFontFile:
        srcCharMap = getTTCharMap(
            args.srcFontFile.rstrip('/ '))  # -> { 2126: 'Omegagreek', ...}
    else:
        argparser.error('No source provided (-src-* argument missing)')
    if len(srcCharMap) == 0:
        print('Empty character map', file=sys.stderr)
        sys.exit(1)

    # Find project source dir
    srcDir = ''
    for dstFontPath in args.dstFontsPaths:
        s = os.path.dirname(dstFontPath)
        if not srcDir:
            srcDir = s
        elif srcDir != s:
            raise Exception(
                'All <ufofile>s must be rooted in the same directory')

    # Load font project config
    # load fontbuild configuration
    config = RawConfigParser(dict_type=OrderedDict)
    configFilename = os.path.join(srcDir, 'fontbuild.cfg')
    config.read(configFilename)
    diacriticsFile = configFindResFile(config, srcDir, 'diacriticfile')

    for dstFontPath in args.dstFontsPaths:
        dstFont = OpenFont(dstFontPath)
        dstCharMap = dstFont.getCharacterMapping(
        )  # -> { 2126: [ 'Omega', ...], ...}
        dstRevCharMap = revCharMap(dstCharMap)  # { 'Omega': 2126, ...}
        srcToDstMap = getGlyphNameDifferenceMap(srcCharMap, dstCharMap,
                                                dstRevCharMap)

        stats = Stats()

        groups, glyphToGroups = fixupGroups(dstFontPath, dstRevCharMap,
                                            srcToDstMap, dryRun, stats)
        fixupKerning(dstFontPath, dstRevCharMap, srcToDstMap, groups,
                     glyphToGroups, dryRun, stats)

        # stats
        if args.saveStatsPath or not args.noStats:
            if not args.noStats:
                print('stats for %s:' % dstFontPath)
                print('  Deleted %d groups and %d glyphs.' %
                      (len(stats.removedGroups), len(stats.removedGlyphs)))
                print('  Renamed %d glyphs.' % len(stats.renamedGlyphs))
                print('  Simplified %d kerning pairs.' %
                      len(stats.simplifiedKerningPairs))
            if args.saveStatsPath:
                statsObj = {
                    'deletedGroups': stats.removedGroups,
                    'deletedGlyphs': stats.removedGlyphs,
                    'simplifiedKerningPairs': stats.simplifiedKerningPairs,
                    'renamedGlyphs': stats.renamedGlyphs,
                }
                f = sys.stdout
                try:
                    if args.saveStatsPath != '-':
                        f = open(args.saveStatsPath, 'w')
                    print('Writing stats to', args.saveStatsPath)
                    json.dump(statsObj,
                              sys.stdout,
                              indent=2,
                              separators=(',', ': '))
                finally:
                    if f is not sys.stdout:
                        f.close()
Exemplo n.º 5
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`')