afdko icon indicating copy to clipboard operation
afdko copied to clipboard

psstemhist – return Python object?

Open frankrolf opened this issue 5 years ago • 4 comments

It would be quite useful to have the results of psstemhist available to Python scripting. I have made some headway into a script (which I hoped to use to apply stem data to UFOs automatically). This script just reads the expected .txt files and converts the data within into Python objects.

Here we go:

'''
Show values found in .vstem and .hstem text files
created by psstemhist, and compare them to UFO stem data.
'''

import os
import re
import argparse
import plistlib


class StemInfo(object):
    '''
    save stem attributes in an object
    '''

    def __init__(self, value, count, glyph_list):
        self.value = int(value)
        self.count = int(count)
        self.glyph_list = glyph_list


def get_args():
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )

    parser.add_argument(
        'ufo_dir',
        help='directory with UFOs',
        action='store',
    )

    return parser.parse_args()


def read_stem_file(stem_file_path):
    '''
    Read a stem txt file and convert the data within into objects
    '''
    with open(stem_file_path, 'r') as sf:
        # the first two lines are headers
        stem_data = sf.read().splitlines()[2:]
    stem_values = []
    stem_objects = []
    for line in stem_data:
        line = line.strip()
        stem_count = int(line.split()[0])
        stem_value = int(line.split()[1])
        glyphs = ' '.join(line.split()[2:])
        glyphs = re.sub(r'[\[\]]', '', glyphs)
        glyph_list = glyphs.split()
        si = StemInfo(stem_value, stem_count, glyph_list)
        stem_values.append(stem_value)
        stem_objects.append(si)

    return stem_objects


def get_stems_from_files(ufo_path):
    '''
    Find hstm.txt and vstm.txt files adjacent to UFO, read an convert them.
    '''
    h_stem_file = ufo_path + '.hstm.txt'
    v_stem_file = ufo_path + '.vstm.txt'
    if os.path.exists(h_stem_file):
        h_stems = read_stem_file(h_stem_file)
    else:
        print(h_stem_file, 'does not exist')
        h_stems = []
    if os.path.exists(v_stem_file):
        v_stems = read_stem_file(v_stem_file)
    else:
        print(v_stem_file, 'does not exist')
        v_stems = []
    return v_stems, h_stems


def fancy_box(my_string):
    '''
    surround a string with a box
    '''
    return (
        '┌' + '─' * (len(my_string) + 2) + '┐\n'
        '│' + ' ' + my_string + ' ' + '│\n'
        '└' + '─' * (len(my_string) + 2) + '┘'
    )


def stem_synopsis(stem):
    '''
    nicely-formatted summary of stem attributes
    '''
    return '\t{} | {} | ({})'.format(
        str(stem.value).rjust(4),
        ' '.join(stem.glyph_list),
        stem.count)


def get_existing_stems(ufo):
    '''
    find which stem values may already exist in a UFO file
    '''
    fi_pl_path = os.path.join(ufo, 'fontinfo.plist')
    with open(fi_pl_path, 'rb') as fi_plist:
        fontinfo_dict = plistlib.load(fi_plist)
    vstm_existing = fontinfo_dict.get('postscriptStemSnapV', [])
    hstm_existing = fontinfo_dict.get('postscriptStemSnapH', [])
    return vstm_existing, hstm_existing


args = get_args()
ufos = []
for root, folders, files in os.walk(args.ufo_dir):
    for folder in folders:
        if re.match(r'.*\.ufo', folder) and folder != 'font.ufo':
            ufos.append((os.path.join(root, folder)))
ufos.sort()

for ufo in ufos:

    print(fancy_box(os.path.basename(ufo)))
    vstm_existing, hstm_existing = get_existing_stems(ufo)
    vstm_stemhist, hstm_stemhist = get_stems_from_files(ufo)
    print('existing V stems:', vstm_existing)
    sorted_v_stems = sorted(
        vstm_stemhist, key=lambda s: s.count, reverse=True)

    for stem in sorted_v_stems:
        if stem.count > 1:
            print(stem_synopsis(stem))

    print('existing H stems:', hstm_existing)
    sorted_h_stems = sorted(
        hstm_stemhist, key=lambda s: s.count, reverse=True)

    for stem in sorted_h_stems:
        if stem.count > 1:
            print(stem_synopsis(stem))

    print('-' * 80)

frankrolf avatar Jun 03 '20 12:06 frankrolf

The result looks something like this:

➜  scriptjes python apply_stemhist.py /Users/fgriessh/work/source-serif-pro
┌───────────────────┐
│ SourceSerif_1.ufo │
└───────────────────┘
existing V stems: [85, 95]
	  84 | S a b d h k l m n q one eight | (13)
	  95 | B D E F H I L P R o | (11)
	  85 | L S f h i m n p r t | (10)
	  81 | b d f g h j q s | (9)
	  90 | b d e p q two five nine | (8)
	  94 | B G H Q T U Y | (7)
	  89 | J a t three six nine | (6)
	  83 | e f u four eight | (6)
	  76 | E F T u | (6)
	 109 | C D G O Q | (5)
	 229 | A X Y | (4)
	  82 | a g p | (3)
	  51 | N U | (3)
existing H stems: [56, 41]
	  47 | A B D E F H I J K L M P Q R T U W X Y g | (42)
	  46 | A B D H K P R U V W X Y a c e o s | (20)
	  50 | C E F G L O Q S T V Z three | (18)
	  45 | G N Q R o s zero two three five six eight nine | (15)
	  36 | f h i k l m n p q r x y one | (13)
	  41 | K N U V W X Y g | (9)
	  57 | b d p q | (8)
	 178 | E T Z | (4)
	  39 | k v w x | (4)
	  82 | two five seven | (3)
	  72 | h m n | (3)
	  65 | c e u | (3)
	  42 | e y z | (3)
	 177 | E F | (2)
	 128 | i j | (2)
	  71 | U u | (2)
	  61 | five six | (2)
	  58 | d u | (2)
	  56 | Q R | (2)
	  55 | f t | (2)
	  53 | f t | (2)
	  52 | a t | (2)
	  43 | G z | (2)
	  40 | g y | (2)
--------------------------------------------------------------------------------
┌───────────────────┐
│ SourceSerif_2.ufo │
└───────────────────┘
existing V stems: [190, 200]
	 190 | B b d f h i j k l m n p q r t u two nine | (21)
	 198 | D E F H I L P T U Y | (12)
	 205 | C G O Q W | (7)
	 200 | D M c e o five | (7)
	 202 | B b d e p q | (6)
	 188 | J N v six | (5)
	 187 | b h p q | (4)
	 186 | G m y | (4)
	 185 | R x three nine | (4)
	 165 | G g four | (3)
	  70 | M N | (3)
existing H stems: [74, 60]
	  63 | A B D E F G H I J K L M N P R S T U W X Y | (37)
	  59 | A C E F G K N T V W Y s y | (16)
	  62 | B D H K P R U V W X | (14)
	  51 | f h i k l m n r w x y one | (13)
	  60 | C L O P Q R V Z | (9)
	  54 | V a c e x zero two three nine | (9)
	  55 | o zero three five six eight | (8)
	  64 | E F J M P W | (7)
	  61 | A K O X Y | (7)
	  47 | k v w x y z | (6)
	  99 | U b d p q | (5)
	 107 | h m n u | (4)
	  96 | b d p q | (4)
	 189 | T Z | (3)
	 170 | E F | (3)
	 140 | two five seven | (3)
	  52 | k p q | (3)
	  46 | g y | (3)
	 172 | i j | (2)
	 152 | g y | (2)
	 108 | c e | (2)
	  75 | f t | (2)
	  72 | u six | (2)
	  66 | N | (2)
	  65 | M g | (2)
	  58 | E three | (2)
	  56 | B x | (2)
	  50 | w x | (2)
--------------------------------------------------------------------------------

frankrolf avatar Jun 03 '20 12:06 frankrolf

@frankrolf are you saying you would like to have this functionality integrated into the psautohint package, so you could do something like import psautohint and then set-up and run the stemhist functionality, which would return some Python object containing these results? That seems totally feasible. I guess I would leave the text formatting of the values to the user, though 😺

josh-hadley avatar Jun 09 '20 23:06 josh-hadley

@frankrolf the snippet below prints the dictionary of the values you're after. This works with psautohint v2.1.2, but beware that the get_fontinfo_list call will need to be updated once the change made in adobe-type-tools/psautohint#276 ships.

from psautohint import FontParseError

from psautohint.autohint import (
    openFile,
    get_fontinfo_list,
    get_glyph_list,
    get_glyph_reports,
)


class PSAutohintOptions(object):
    def __init__(self):
        self.read_hints = False
        self.allowChanges = False
        self.writeToDefaultLayer = False
        self.noHintSub = False
        self.excludeGlyphList = []
        self.hCounterGlyphs = []
        self.vCounterGlyphs = []
        self.round_coords = True
        self.hintAll = True
        self.noFlex = True
        self.allow_no_blues = True
        self.logOnly = True
        self.printDefaultFDDict = False
        self.printFDDictList = False
        self.inputPaths = []
        self.outputPaths = []
        self.report_all_stems = True
        # For zones instead of stems adjust the values below
        self.report_zones = False
        self.report_stems = True


font_file_path = 'myfont.ufo'
glyph_names_list = ['A', 'a', 'ampersand']


options = PSAutohintOptions()
options.glyphList = glyph_names_list

try:
    psfont = openFile(font_file_path, options)
    glyph_list = get_glyph_list(options, psfont, font_file_path)
    fontinfo_list = get_fontinfo_list(
        options, psfont, None, glyph_list, False)

    reports = get_glyph_reports(
        options, psfont, glyph_list, fontinfo_list)

    print(reports.glyphs)

except FontParseError as e:
    print(e)

miguelsousa avatar Dec 01 '20 18:12 miguelsousa

This should be a simple matter of plumbing now -- deciding on what arguments to use to retrieve the GlyphReport object.

skef avatar Aug 14 '23 21:08 skef