def action(self, args): overhang_padding, glyphs = args parser = self.parser for c in ["inits", "medis"]: if c not in parser.fontfeatures.namedClasses: raise ValueError("Please define @%s class before calling") medis = parser.fontfeatures.namedClasses["medis"] inits = parser.fontfeatures.namedClasses["inits"] overhangers = glyphs.resolve(parser.fontfeatures, parser.font) binned_medis = bin_glyphs_by_metric(parser.font, medis, "run", bincount=8) binned_inits = bin_glyphs_by_metric(parser.font, inits, "run", bincount=8) rules = [] maxchainlength = 0 longeststring = [] for yb in overhangers: entry_anchor = parser.fontfeatures.anchors[yb]["entry"] overhang = max( -get_glyph_metrics(parser.font, yb)["rsb"], get_glyph_metrics(parser.font, yb)["xMax"] - entry_anchor[0], ) workqueue = [[x] for x in binned_inits] while workqueue: string = workqueue.pop(0) totalwidth = sum([max(x[1], failsafe_min_run) for x in string]) if totalwidth > overhang or len(string) > failsafe_max_length: continue adjustment = overhang - totalwidth + int(overhang_padding) postcontext = [x[0] for x in string[:-1]] + [[yb]] input_ = string[-1] example = [input_[0][0]] + [x[0] for x in postcontext] warnings.warn( "For glyphs in %s, overhang=%i totalwidth=%i adjustment=%i" % (example, overhang, totalwidth, adjustment)) maxchainlength = max(maxchainlength, len(string)) rules.append( fontFeatures.Positioning( [input_[0]], [fontFeatures.ValueRecord(xAdvance=int(adjustment))], postcontext=postcontext, )) for medi in binned_medis: workqueue.append([medi] + string) warnings.warn("Bari Ye collision maximum chain length was %i glyphs" % maxchainlength) return [fontFeatures.Routine(rules=rules, flags=8)]
def _get_metrics(self, glyph, metric=None): metrics = get_glyph_metrics(self.parser.font, glyph) if metric is not None: if metric not in TESTVALUE_METRICS: raise ValueError("Unknown metric '%s'" % metric) else: return metrics[metric] else: return metrics
def meets_predicate(self, glyphname, predicate, parser): inverted = predicate["inverted"] metric = predicate["predicate"] if metric == "hasanchor": anchor = predicate["value"] truth = glyphname in parser.fontfeatures.anchors and anchor in parser.fontfeatures.anchors[ glyphname] elif metric == "category": cat = predicate["value"] truth = parser.font[glyphname].category == cat elif metric == "hasglyph": truth = re.sub(predicate["value"]["replace"], predicate["value"]["with"], glyphname) in parser.font else: comp = predicate["comparator"] if isinstance(predicate["value"], dict): v = predicate["value"] testvalue_metrics = get_glyph_metrics(parser.font, v["glyph"]) if v["metric"] not in testvalue_metrics: raise ValueError("Unknown metric '%s'" % metric) testvalue = testvalue_metrics[v["metric"]] else: testvalue = int(predicate["value"]) metrics = get_glyph_metrics(parser.font, glyphname) if metric not in metrics: raise ValueError("Unknown metric '%s'" % metric) value = metrics[metric] if comp == ">": truth = value > testvalue elif comp == "<": truth = value < testvalue elif comp == ">=": truth = value >= testvalue elif comp == "<=": truth = value <= testvalue else: raise ValueError("Bad comparator %s (can't happen?)" % comp) if inverted: truth = not truth return truth
def compute_threshold(self, parser, below_dots): from fontFeatures.ttLib import unparse font = parser.font behforms = list( filter( lambda g: g.startswith("BEm") or g.startswith("BEi"), parser.font.keys(), )) bottomOfDot = statistics.mean( [get_glyph_metrics(font, x)["yMin"] for x in below_dots]) if hasattr(parser.fontfeatures, "anchors"): anchor1_y = statistics.mean([ parser.fontfeatures.anchors[x]["_bottom"][1] for x in below_dots if x in parser.fontfeatures.anchors ]) anchor2_y = statistics.mean([ parser.fontfeatures.anchors[x]["bottom"][1] for x in behforms if x in parser.fontfeatures.anchors and "bottom" in parser.fontfeatures.anchors[x] ]) else: # Find the anchors ff2 = unparse(font) if "mark" not in ff2.features: raise ValueError("No mark positioning for font!") rules = list( filter( lambda r: below_dots[0] in r.marks and any( [m in r.bases for m in behforms]), [x for y in ff2.features["mark"] for x in y.rules], )) if len(rules) < 1: raise ValueError("No nukta positioning?") r = rules[0] anchor1_y = r.marks[below_dots[0]][1] anchor2_y = statistics.mean( [r.bases[x][1] for x in behforms if x in r.bases]) displacement = anchor2_y - anchor1_y return -(bottomOfDot + displacement)
def test(self, glyphset, font, infocache): matches = [] if self.type == "Name": # print(self.comparator, self.value) if self.comparator == "begins": matches = [x for x in glyphset if x.startswith(self.value)] elif self.comparator == "ends": matches = [x for x in glyphset if x.endswith(self.value)] elif self.comparator == "matches": try: matches = [x for x in glyphset if re.search(self.value, x)] except Exception as e: matches = [] # XXX HasAnchor # XXX Is member of # XXX Is Category if self.metric: matches = [] try: for g in glyphset: if g not in infocache: infocache[g] = {"metrics": get_glyph_metrics(font, g)} got = infocache[g]["metrics"][self.type] expected, comp = int(self.value), self.comparator if (comp == ">" and got > expected) or ( comp == "<" and got < expected) or ( comp == "=" and got == expected) or ( comp == "<=" and got <= expected) or ( comp == ">=" and got >= expected): matches.append(g) except Exception as e: print(e) pass return matches
def action(self, args): strategy, below_dots = args parser = self.parser for c in ["inits", "medis", "bariye", "behs"]: if c not in parser.fontfeatures.namedClasses: raise ValueError("Please define @%s class before calling" % c) alwaysDrop = strategy == "AlwaysDrop" # OK, here's the plan of attack for bari ye. # First, we find the length of the longest possible sequence. # Smallest medi * n + smallest beh + smallest init -> length of bari yeh right tail medis = parser.fontfeatures.namedClasses["medis"] inits = parser.fontfeatures.namedClasses["inits"] behs = parser.fontfeatures.namedClasses["behs"] below_dots = below_dots.resolve(parser.fontfeatures, parser.font) smallest_medi_width = max( failsafe_min_run, min([get_run(parser.font, g) for g in medis])) smallest_init_beh_width = min( [get_run(parser.font, g) for g in (set(behs) & set(inits))]) smallest_init_beh = min( (set(behs) & set(inits)), key=lambda g: get_run(parser.font, g), ) warnings.warn("Smallest medi width is %i" % smallest_medi_width) smallest_medi = min(medis, key=lambda g: get_run(parser.font, g)) routines = [] for bariye in parser.fontfeatures.namedClasses["bariye"]: warnings.warn("BariYe computation for %s" % bariye) entry_anchor = parser.fontfeatures.anchors[bariye]["entry"] bariye_tail = max( -get_glyph_metrics(parser.font, bariye)["rsb"], get_glyph_metrics(parser.font, bariye)["xMax"] - entry_anchor[0], ) # bariye_tail += max(get_glyph_metrics(parser.font, dot)["width"] for dot in below_dots) / 2 # # Increase tail by half the width of the widest nukta # bariye_tail += ( # max( # [ # get_glyph_metrics(parser.font, g)["xMax"] # - get_glyph_metrics(parser.font, g)["xMin"] # for g in below_dots # ] # ) # / 2 # ) # Consider two cases. # First, the case where the beh is init and full of short medis maximum_sequence_length_1 = 1 + math.ceil( (bariye_tail - smallest_init_beh_width) / smallest_medi_width) warnings.warn("Longest init beh sequence: %s" % " ".join([smallest_medi] * (maximum_sequence_length_1 - 1) + [smallest_init_beh])) # Second, the case where the init is not beh, but there are a bunch of medis, # one (or more) of which is a medi beh smallest_init_nonbeh_width = min([ max(get_rise(parser.font, g), failsafe_min_run) for g in (set(inits) - set(behs)) ]) smallest_medi_beh_width = min([ max(get_rise(parser.font, g), failsafe_min_run) for g in (set(medis) & set(behs)) ]) maximum_sequence_length_2 = 2 + math.ceil( (bariye_tail - smallest_init_nonbeh_width - smallest_medi_beh_width) / smallest_medi_width) maximum_sequence_length = min( failsafe_max_length, max(maximum_sequence_length_1, maximum_sequence_length_2)) warnings.warn("Max sequence width is %i" % maximum_sequence_length) # Next, let's create a chain rule for all nukta sequences dropBYsRoutine = fontFeatures.Routine(flags=0x0010) dropBYsRoutine.markFilteringSet = below_dots dropADotRoutine = fontFeatures.Routine() # Substitute those not ending .yb with those ending .yb below_dots_non_yb = list( sorted(filter(lambda x: not x.endswith(".yb"), below_dots))) below_dots_yb = list( sorted(filter(lambda x: x.endswith(".yb"), below_dots))) if len(below_dots_non_yb) != len(below_dots_yb): raise ValueError( "Mismatch in @below_dots: %s has .yb suffix, %s does not" % (below_dots_yb, below_dots_non_yb)) dropADotRoutine.addRule( fontFeatures.Substitution([below_dots_non_yb], [below_dots_yb])) maybeDropDotRoutine = fontFeatures.Routine(flags=0x0010) maybeDropDotRoutine.markFilteringSet = below_dots binned_medis = bin_glyphs_by_metric(parser.font, medis, "run", bincount=accuracy1) binned_inits = bin_glyphs_by_metric(parser.font, inits, "run", bincount=accuracy1) queue = [[[[bariye]]]] bin2mk = {"0": None, "1": below_dots} while len(queue) > 0: consideration = queue.pop(0) # import code; code.interact(local=locals()) seq_length = sum([s[1] for s in consideration[:-1]]) repsequence = [(s[0][0], s[1]) for s in consideration[:-1]] sequence = [s[0] for s in consideration] warnings.warn("Sequence %s total %i bariye_tail %i" % (repsequence, seq_length, bariye_tail)) if seq_length > bariye_tail or len( consideration) > maximum_sequence_length: continue lu = [None] * len(sequence) if alwaysDrop: lu[0] = [dropADotRoutine] else: lu[0] = [maybeDropDotRoutine] for j in range(0, 2**(len(sequence) - 1)): binary = "{0:0%ib}" % (len(sequence) - 1) marksequence = [bin2mk[x] for x in list(binary.format(j))] # import code; code.interact(local=locals()) newsequence = dropnone(interleave(sequence, marksequence)) chainrule = fontFeatures.Chaining([below_dots], postcontext=newsequence, lookups=lu) # We don't combine the bins here precisely because they're # disjoint sets and that means they can be expressed as a # format 2 class-based rule! Wonderful! dropBYsRoutine.addRule(chainrule) for m in binned_medis: queue.append([list(m)] + consideration) if not alwaysDrop: # Check to see if it can fit in the gap, and only move it if it can't medis_by_rise = bin_glyphs_by_metric(parser.font, medis, "rise", bincount=accuracy2) queue = [[[[bariye], get_glyph_metrics(parser.font, bariye)["rise"]]]] ybClearance = self.get_yb_clearance(parser, bariye) gapRequired = self.compute_threshold(parser, below_dots) - ybClearance warnings.warn( "%i units of rise are required to fit a nukta in the gap" % gapRequired) while len(queue) > 0: consideration = queue.pop(0) total_rise = sum([s[1] for s in consideration]) repsequence = [(s[0][0], s[1]) for s in consideration] # warnings.warn("Sequence %s total rise %i required %i" % (repsequence, total_rise, gapRequired)) if (total_rise > gapRequired or len(consideration) > maximum_sequence_length): # warnings.warn("Does not drop") continue sequence = [s[0] for s in consideration] lu = [None] * len(sequence) lu[0] = [dropADotRoutine] chainrule = fontFeatures.Chaining([below_dots], postcontext=sequence, lookups=lu) maybeDropDotRoutine.addRule(chainrule) # print("Drops %s" % chainrule.asFea()) for m in medis_by_rise: if total_rise + m[1] < gapRequired: queue.append([list(m)] + consideration) # Add all the routines to the parser parser.fontfeatures.routines.append(dropADotRoutine) if not alwaysDrop: parser.fontfeatures.routines.append(maybeDropDotRoutine) routines.append(dropBYsRoutine) return routines
if args.prev: preview = "" for i in range(num_chars): preview += str(chr(i + start_char)) image = Image.new("RGB", (1000, 1000)) draw = ImageDraw.Draw(image) draw.text((0, 0), preview, font=font) image = image.crop(image.getbbox()) image.save(args.tf + "_prev" + str(args.pt[pt]) + ".png") pixels = image.load() # Find the max and min values ymax = 0 ymin = 0 for z in range(num_chars): m = glyphtools.get_glyph_metrics(font_info, font_info.getGlyphName(start_id + z)) new_ymax = (font_size * m['yMax'] / scale) new_ymin = (font_size * m['yMin'] / scale) ymin = round(min(ymin, new_ymin)) ymax = round(max(ymax, new_ymax)) maxmax = round(abs(ymin) + abs(ymax)) # We are adding a new font size. Allign the start address if curr_offset & 3: curr_offset = (curr_offset + 3) & ~3 # Add the glyph header indicating the global size of the font font_data += abs(ymin).to_bytes(1, byteorder="little", signed=False) font_data += abs(ymax).to_bytes(1, byteorder="little", signed=False) font_data.append(0) font_data.append(0)