Skip to content

Commit

Permalink
Merge pull request #100 from googlefonts/more-options
Browse files Browse the repository at this point in the history
More options
  • Loading branch information
m4rc1e authored Oct 26, 2023
2 parents 82a5280 + caf46ea commit 1cc3d5d
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 58 deletions.
20 changes: 19 additions & 1 deletion src/diffenator2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os
from ninja.ninja_syntax import Writer
from diffenator2.renderer import FONT_SIZE
from diffenator2.utils import dict_coords_to_string
from diffenator2.font import DFont, get_font_styles
from diffenator2.utils import partition
Expand Down Expand Up @@ -102,6 +103,9 @@ def ninja_diff(
filter_styles: str = "",
pt_size: int = 20,
threshold: float = THRESHOLD,
precision: int = FONT_SIZE,
no_words: bool = False,
no_tables: bool = False,
):
if not os.path.exists(out):
os.mkdir(out)
Expand All @@ -120,6 +124,9 @@ def ninja_diff(
filter_styles,
pt_size,
threshold=threshold,
precision=precision,
no_words=no_words,
no_tables=no_tables,
)
return

Expand Down Expand Up @@ -152,6 +159,9 @@ def ninja_diff(
filter_styles,
pt_size,
threshold=threshold,
precision=precision,
no_words=no_words,
no_tables=no_tables,
)

def _ninja_diff(
Expand All @@ -167,6 +177,9 @@ def _ninja_diff(
filter_styles: str = "",
pt_size: int = 20,
threshold: float = THRESHOLD,
precision: int = FONT_SIZE,
no_words: bool = False,
no_tables: bool = False,
):
w = Writer(open(NINJA_BUILD_FILE, "w", encoding="utf8"))
# Setup rules
Expand All @@ -184,9 +197,13 @@ def _ninja_diff(
w.newline()

w.comment("Run diffenator VF")
diff_cmd = f'_diffenator $font_before $font_after -t $threshold -o $out -ch "$characters"'
diff_cmd = f'_diffenator $font_before $font_after --font-size $precision -t $threshold -o $out -ch "$characters"'
if user_wordlist:
diff_cmd += ' --user-wordlist "$user_wordlist"'
if no_words:
diff_cmd += ' --no-words'
if no_tables:
diff_cmd += ' --no-tables'
diff_inst_cmd = diff_cmd + " --coords $coords"
w.rule("diffenator", diff_cmd)
w.rule("diffenator-inst", diff_inst_cmd)
Expand Down Expand Up @@ -221,6 +238,7 @@ def _ninja_diff(
out=style,
threshold=str(threshold),
characters=characters,
precision=str(precision),
)
if user_wordlist:
diff_variables["user_wordlist"] = user_wordlist
Expand Down
7 changes: 7 additions & 0 deletions src/diffenator2/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from diffenator2 import ninja_diff, ninja_proof, THRESHOLD, NINJA_BUILD_FILE
from diffenator2.font import DFont
from diffenator2.html import build_index_page
from diffenator2.renderer import FONT_SIZE



Expand Down Expand Up @@ -47,6 +48,9 @@ def main(**kwargs):
diff_parser.add_argument("--fonts-after", "-fa", nargs="+", required=True)
diff_parser.add_argument("--no-diffenator", default=False, action="store_true")
diff_parser.add_argument("--threshold", "-t", type=float, default=THRESHOLD)
diff_parser.add_argument("--precision", default=FONT_SIZE)
diff_parser.add_argument("--no-tables", action="store_true", help="Skip diffing font tables")
diff_parser.add_argument("--no-words", action="store_true", help="Skip diffing wordlists")
args = parser.parse_args()

if os.path.exists(NINJA_BUILD_FILE):
Expand Down Expand Up @@ -80,6 +84,9 @@ def main(**kwargs):
filter_styles=args.filter_styles,
pt_size=args.pt_size,
threshold=args.threshold,
precision=args.precision,
no_words=args.no_words,
no_tables=args.no_tables,
)
else:
raise NotImplementedError(f"{args.command} not supported")
Expand Down
66 changes: 54 additions & 12 deletions src/diffenator2/_diffenator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
diffenator2
"""
from __future__ import annotations
from diffenator2.utils import string_coords_to_dict, re_filter_characters, characters_in_string
from diffenator2.renderer import FONT_SIZE
from diffenator2.utils import (
string_coords_to_dict,
re_filter_characters,
characters_in_string,
)
from diffenator2.font import DFont
from diffenator2.matcher import FontMatcher
from pkg_resources import resource_filename
Expand All @@ -15,38 +20,66 @@


class DiffFonts:
def __init__(self, matcher, threshold=0.01):
def __init__(self, matcher, threshold=0.01, font_size=28, words=True, tables=True):
self.old_font = matcher.old_fonts[0]
self.new_font = matcher.new_fonts[0]

self.old_style = matcher.old_styles[0]
self.new_style = matcher.new_styles[0]
self.threshold = threshold
self.do_words = words
self.do_tables = tables
self.font_size = font_size

def diff_all(self):
skip = frozenset(["diff_strings", "diff_all"])
diff_funcs = [f for f in dir(self) if f.startswith("diff_") if f not in skip]
for f in diff_funcs:
getattr(self,f)()
getattr(self, f)()

def diff_tables(self):
if not self.do_tables:
self.tables = jfont.Diff({}, {})
return
self.tables = jfont.Diff(self.old_font.jFont, self.new_font.jFont)

def diff_strings(self, fp):
self.strings = test_words(fp, self.old_font, self.new_font, threshold=self.threshold)
self.strings = test_words(
fp,
self.old_font,
self.new_font,
threshold=self.threshold,
font_size=self.font_size,
)

def diff_words(self):
self.glyph_diff = test_fonts(self.old_font, self.new_font, threshold=self.threshold)
self.glyph_diff = test_fonts(
self.old_font,
self.new_font,
threshold=self.threshold,
do_words=self.do_words,
font_size=self.font_size,
)

def filter_characters(self, characters):
diff_words = self.glyph_diff["words"]
for cat in diff_words:
diff_words[cat] = [i for i in diff_words[cat] if characters_in_string(i.string, characters)]

diff_words[cat] = [
i for i in diff_words[cat] if characters_in_string(i.string, characters)
]

diff_glyphs = self.glyph_diff["glyphs"]
diff_glyphs.new = [g for g in diff_glyphs.new if characters_in_string(g.string, characters)]
diff_glyphs.missing = [g for g in diff_glyphs.missing if characters_in_string(g.string, characters)]
diff_glyphs.modified = [g for g in diff_glyphs.modified if characters_in_string(g.string, characters)]
diff_glyphs.new = [
g for g in diff_glyphs.new if characters_in_string(g.string, characters)
]
diff_glyphs.missing = [
g for g in diff_glyphs.missing if characters_in_string(g.string, characters)
]
diff_glyphs.modified = [
g
for g in diff_glyphs.modified
if characters_in_string(g.string, characters)
]

def to_html(self, templates, out):
diffenator_report(self, templates, dst=out)
Expand All @@ -67,7 +100,10 @@ def main():
)
parser.add_argument("--coords", "-c", default={})
parser.add_argument("--threshold", "-t", default=THRESHOLD, type=float)
parser.add_argument("--font-size", "-pt", default=FONT_SIZE, type=int)
parser.add_argument("--characters", "-ch", default=".*")
parser.add_argument("--no-words", action="store_true")
parser.add_argument("--no-tables", action="store_true")
parser.add_argument("--out", "-o", default="out", help="Output html path")
args = parser.parse_args()

Expand All @@ -79,11 +115,17 @@ def main():
matcher.diffenator(coords)
matcher.upms()

diff = DiffFonts(matcher, threshold=args.threshold)
diff = DiffFonts(
matcher,
words=not args.no_words,
tables=not args.no_tables,
threshold=args.threshold,
font_size=args.font_size
)
diff.diff_all()
if args.user_wordlist:
diff.diff_strings(args.user_wordlist)

characters = re_filter_characters(new_font, args.characters)
diff.filter_characters(characters)
diff.to_html(args.template, args.out)
Expand Down
72 changes: 53 additions & 19 deletions src/diffenator2/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import uharfbuzz as hb
import os
from diffenator2 import THRESHOLD
from diffenator2.renderer import PixelDiffer
from diffenator2.renderer import FONT_SIZE, PixelDiffer
from diffenator2.template_elements import Word, WordDiff, Glyph, GlyphDiff
from pkg_resources import resource_filename
import tqdm
Expand All @@ -16,6 +16,7 @@

# Hashing strategies for elements of a Harfbuzz buffer


def gid_pos_hash(info, pos):
return f"gid={info.codepoint}, pos={pos.position}<br>"

Expand All @@ -38,32 +39,35 @@ def gid_pos_hash(info, pos):
ot_to_dir = {None: "ltr", "arab": "rlt", "hebr": "rtl"}




@dataclass
class GlyphItems:
missing: list
new: list
modified: list


def test_fonts(font_a, font_b, threshold=THRESHOLD):
glyphs = test_font_glyphs(font_a, font_b, threshold=threshold)
def test_fonts(font_a, font_b, threshold=THRESHOLD, do_words=True, font_size=FONT_SIZE):
glyphs = test_font_glyphs(font_a, font_b, threshold=threshold, font_size=font_size)
skip_glyphs = glyphs.missing + glyphs.new
words = test_font_words(font_a, font_b, skip_glyphs, threshold=threshold)
if do_words:
words = test_font_words(
font_a, font_b, skip_glyphs, threshold=threshold, font_size=font_size
)
else:
words = {}
return {"glyphs": glyphs, "words": words}


def test_font_glyphs(font_a, font_b, threshold=THRESHOLD):
def test_font_glyphs(font_a, font_b, threshold=THRESHOLD, font_size=FONT_SIZE):
cmap_a = set(chr(c) for c in font_a.ttFont.getBestCmap())
cmap_b = set(chr(c) for c in font_b.ttFont.getBestCmap())
missing_glyphs = set(Glyph(c) for c in cmap_a - cmap_b)
new_glyphs = set(Glyph(c) for c in cmap_b - cmap_a)
same_glyphs = cmap_a & cmap_b
skip_glyphs = missing_glyphs | new_glyphs
modified_glyphs = []
differ = PixelDiffer(font_a, font_b)
for g in same_glyphs:
differ = PixelDiffer(font_a, font_b, font_size=font_size)
for g in tqdm.tqdm(same_glyphs):
pc, diff_map = differ.diff(g)
if pc > threshold:
glyph = GlyphDiff(g, "%.2f" % pc, diff_map)
Expand All @@ -77,7 +81,9 @@ def test_font_glyphs(font_a, font_b, threshold=THRESHOLD):
)


def test_font_words(font_a, font_b, skip_glyphs=set(), threshold=THRESHOLD):
def test_font_words(
font_a, font_b, skip_glyphs=set(), threshold=THRESHOLD, font_size=FONT_SIZE
):
from youseedee import ucd_data
from collections import defaultdict

Expand All @@ -98,21 +104,40 @@ def test_font_words(font_a, font_b, skip_glyphs=set(), threshold=THRESHOLD):
if not os.path.exists(wordlist):
print(f"No wordlist for {script}")
continue
res[script] = test_words(wordlist, font_a, font_b, skip_glyphs, threshold=threshold)
res[script] = test_words(
wordlist,
font_a,
font_b,
skip_glyphs,
threshold=threshold,
font_size=font_size,
)
return res


def parse_wordlist(fp):
from diffenator2.shape import Word as TemplateWord

results = []
with open(fp, encoding="utf8") as doc:
lines = doc.read().split("\n")
for line in lines:
items = line.split(",")
try:
results.append(TemplateWord(string=items[0], script=items[1], lang=items[2], ot_features={k: True for k in items[3:]}))
results.append(
TemplateWord(
string=items[0],
script=items[1],
lang=items[2],
ot_features={k: True for k in items[3:]},
)
)
except IndexError:
results.append(TemplateWord(string=items[0], script="dflt", lang=None, ot_features={}))
results.append(
TemplateWord(
string=items[0], script="dflt", lang=None, ot_features={}
)
)
return results


Expand All @@ -123,12 +148,13 @@ def test_words(
skip_glyphs=set(),
hash_func=gid_pos_hash,
threshold=THRESHOLD,
font_size=FONT_SIZE,
):
res = set()

seen_gids = defaultdict(int)

differ = PixelDiffer(font_a, font_b)
differ = PixelDiffer(font_a, font_b, font_size=font_size)
word_list = parse_wordlist(word_file)
for i, word in tqdm.tqdm(enumerate(word_list), total=len(word_list)):
differ.set_script(word.script)
Expand All @@ -137,8 +163,14 @@ def test_words(

# split sentences into individual script segments. This mimmics the
# same behaviour as dtp apps, web browsers etc
for segment, script, _, _, in textSegments(word.string)[0]:

for (
segment,
script,
_,
_,
) in textSegments(
word.string
)[0]:
if any(c.string in segment for c in skip_glyphs):
continue

Expand All @@ -148,7 +180,10 @@ def test_words(
buf_b = differ.renderer_b.shape(segment)
word_b = Word.from_buffer(segment, buf_b)

gid_hashes = [hash_func(i, j) for i, j in zip(buf_b.glyph_infos, buf_b.glyph_positions)]
gid_hashes = [
hash_func(i, j)
for i, j in zip(buf_b.glyph_infos, buf_b.glyph_positions)
]
# I'm not entirely convinced this is a valid test; but it seems to
# work and speeds things up a lot...
if all(gid_hash in seen_gids for gid_hash in gid_hashes):
Expand All @@ -158,7 +193,7 @@ def test_words(
word_a = Word.from_buffer(segment, buf_a)

# skip any words which cannot be shaped correctly
if any([g.codepoint == 0 for g in buf_a.glyph_infos+buf_b.glyph_infos]):
if any([g.codepoint == 0 for g in buf_a.glyph_infos + buf_b.glyph_infos]):
continue

pc, diff_map = differ.diff(segment)
Expand All @@ -183,4 +218,3 @@ def test_words(
)
)
return [w[1] for w in sorted(res, key=lambda k: k[0], reverse=True)]

Loading

0 comments on commit 1cc3d5d

Please sign in to comment.